stellarium-ts 0.11.0 → 0.13.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 (77) hide show
  1. package/dist/client/operatorClient.d.ts +10 -4
  2. package/dist/client/operatorClient.d.ts.map +1 -1
  3. package/dist/client/operatorClient.js +51 -7
  4. package/dist/crypto/utils.d.ts +9 -0
  5. package/dist/crypto/utils.d.ts.map +1 -1
  6. package/dist/crypto/utils.js +30 -1
  7. package/dist/db/db.d.ts +5 -4
  8. package/dist/db/db.d.ts.map +1 -1
  9. package/dist/db/db.js +88 -12
  10. package/dist/grant/grant.d.ts +132 -0
  11. package/dist/grant/grant.d.ts.map +1 -0
  12. package/dist/grant/grant.js +158 -0
  13. package/dist/grant/repository.d.ts +57 -0
  14. package/dist/grant/repository.d.ts.map +1 -0
  15. package/dist/grant/repository.js +105 -0
  16. package/dist/grant/service.d.ts +141 -0
  17. package/dist/grant/service.d.ts.map +1 -0
  18. package/dist/grant/service.js +431 -0
  19. package/dist/index.d.ts +6 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +5 -1
  22. package/dist/peer/repository.d.ts +33 -2
  23. package/dist/peer/repository.d.ts.map +1 -1
  24. package/dist/peer/repository.js +43 -21
  25. package/dist/resource/invoker.d.ts +21 -9
  26. package/dist/resource/invoker.d.ts.map +1 -1
  27. package/dist/resource/invoker.js +69 -16
  28. package/dist/resource/repository.d.ts +47 -8
  29. package/dist/resource/repository.d.ts.map +1 -1
  30. package/dist/resource/repository.js +122 -67
  31. package/dist/routes.d.ts +8 -0
  32. package/dist/routes.d.ts.map +1 -1
  33. package/dist/routes.js +303 -49
  34. package/dist/server.d.ts +62 -0
  35. package/dist/server.d.ts.map +1 -1
  36. package/dist/server.js +119 -18
  37. package/dist/service.d.ts +24 -1
  38. package/dist/service.d.ts.map +1 -1
  39. package/dist/service.js +9 -6
  40. package/dist/startup.d.ts +14 -0
  41. package/dist/startup.d.ts.map +1 -1
  42. package/dist/startup.js +90 -76
  43. package/dist/tests/client/operatorClient.test.d.ts +2 -0
  44. package/dist/tests/client/operatorClient.test.d.ts.map +1 -0
  45. package/dist/tests/client/operatorClient.test.js +96 -0
  46. package/dist/tests/db/db.test.d.ts +2 -0
  47. package/dist/tests/db/db.test.d.ts.map +1 -0
  48. package/dist/tests/db/db.test.js +64 -0
  49. package/dist/tests/grant/grant.test.d.ts +2 -0
  50. package/dist/tests/grant/grant.test.d.ts.map +1 -0
  51. package/dist/tests/grant/grant.test.js +133 -0
  52. package/dist/tests/grant/service.test.d.ts +2 -0
  53. package/dist/tests/grant/service.test.d.ts.map +1 -0
  54. package/dist/tests/grant/service.test.js +415 -0
  55. package/dist/tests/helpers.d.ts +13 -0
  56. package/dist/tests/helpers.d.ts.map +1 -0
  57. package/dist/tests/helpers.js +49 -0
  58. package/dist/tests/integration/network.test.d.ts +2 -0
  59. package/dist/tests/integration/network.test.d.ts.map +1 -0
  60. package/dist/tests/integration/network.test.js +188 -0
  61. package/dist/tests/peer/repository.test.d.ts +2 -0
  62. package/dist/tests/peer/repository.test.d.ts.map +1 -0
  63. package/dist/tests/peer/repository.test.js +50 -0
  64. package/dist/tests/resource/invoker.test.js +132 -5
  65. package/dist/tests/resource/repository.test.d.ts +2 -0
  66. package/dist/tests/resource/repository.test.d.ts.map +1 -0
  67. package/dist/tests/resource/repository.test.js +151 -0
  68. package/dist/tests/routes.test.d.ts +2 -0
  69. package/dist/tests/routes.test.d.ts.map +1 -0
  70. package/dist/tests/routes.test.js +565 -0
  71. package/dist/tests/server.test.d.ts +2 -0
  72. package/dist/tests/server.test.d.ts.map +1 -0
  73. package/dist/tests/server.test.js +37 -0
  74. package/dist/tests/startup.test.d.ts +2 -0
  75. package/dist/tests/startup.test.d.ts.map +1 -0
  76. package/dist/tests/startup.test.js +75 -0
  77. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"invoker.d.ts","sourceRoot":"","sources":["../../src/resource/invoker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,6BAA6B,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEzF,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAErD,MAAM,WAAW,cAAc;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACjC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,EAC1C,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,6BAA6B,EAAE;;;;;;;;;;EAI9C;AAED,qBAAa,0BAA2B,YAAW,eAAe;IAElD,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IAAsB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAAU,OAAO,CAAC,QAAQ,CAAC,SAAS;IAAU,OAAO,CAAC,QAAQ,CAAC,UAAU;IAAhL,YAA6B,kBAAkB,EAAE,kBAAkB,EAAmB,iBAAiB,EAAE,MAAM,EAAmB,SAAS,EAAE,MAAM,EAAmB,UAAU,EAAE,MAAM,EAAI;IAC5L;;;;;;;;;;OAUG;IACG,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,6BAA6B,EAAE,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,GAAG,CAAC;QAAC,KAAK,EAAE;YAAE,OAAO,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;KAAC,CAAC,CAmD7J;IAED,oBAAoB,CAAC,gBAAgB,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,6BAA6B,EAAE,GAAG,cAAc,CAOtK;CACJ"}
1
+ {"version":3,"file":"invoker.d.ts","sourceRoot":"","sources":["../../src/resource/invoker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,6BAA6B,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEzF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAErD,MAAM,WAAW,cAAc;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC1B,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,6BAA6B,EAAE,GAC5C,MAAM,CAER;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC;;;;;;;;EAG/E;AAED,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAC7C,eAAO,MAAM,6BAA6B,MAAM,CAAC;AACjD,wEAAwE;AACxE,eAAO,MAAM,0BAA0B,QAAS,CAAC;AA2BjD,qBAAa,0BAA2B,YAAW,eAAe;IAGlD,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IAAsB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAAU,OAAO,CAAC,QAAQ,CAAC,SAAS;IAAU,OAAO,CAAC,QAAQ,CAAC,UAAU;IAAU,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;IADxN,2EAA2E;IAC3E,YAA6B,kBAAkB,EAAE,kBAAkB,EAAmB,iBAAiB,EAAE,MAAM,EAAmB,SAAS,EAAE,MAAM,EAAmB,UAAU,EAAE,MAAM,EAAmB,YAAY,CAAC,EAAE,YAAY,YAAA,EAAI;IAC1O;;;;;;;;;;;OAWG;IACG,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,6BAA6B,EAAE,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,GAAG,CAAC;QAAC,KAAK,EAAE;YAAE,OAAO,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;KAAC,CAAC,CAgE7J;IAED,oBAAoB,CAAC,gBAAgB,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,6BAA6B,EAAE,GAAG,cAAc,CAQ5L;CACJ"}
@@ -1,27 +1,67 @@
1
- import { signData } from "../crypto/utils.js";
1
+ import { hashData, signData } from "../crypto/utils.js";
2
2
  /**
3
- * The object `createPaymentVoucher` signs. `signature` is excluded, and
4
- * `resourcePath`, `resourceName`, and `parameters` come from the invocation,
5
- * not from the voucher itself. Key order is part of the signed bytes.
3
+ * Hash binding a voucher to one function call. Providers recompute it from the
4
+ * request they receive and compare it to `voucher.invocationHash`.
5
+ * Key order is part of the hashed bytes.
6
6
  */
7
- export function paymentVoucherPayload(voucher, resourcePath, resourceName, parameters) {
8
- const { requestId, invocationId, amount, recipientAddress, expiresAt, publicKey } = voucher;
9
- return { requestId, invocationId, amount, recipientAddress, expiresAt, resourcePath, resourceName, parameters, publicKey };
7
+ export function invocationHash(resourcePath, resourceName, functionName, parameters) {
8
+ return hashData({ resourcePath, resourceName, functionName, parameters });
9
+ }
10
+ /**
11
+ * The object `createPaymentVoucher` signs: every voucher field except `signature`.
12
+ * Key order is part of the signed bytes.
13
+ */
14
+ export function paymentVoucherPayload(voucher) {
15
+ const { requestId, invocationId, amount, recipientAddress, expiresAt, invocationHash, publicKey } = voucher;
16
+ return { requestId, invocationId, amount, recipientAddress, expiresAt, invocationHash, publicKey };
17
+ }
18
+ export const EXTERNAL_INVOKE_MAX_RETRIES = 3;
19
+ export const EXTERNAL_INVOKE_BASE_DELAY_MS = 500;
20
+ /** Per attempt. A timed-out attempt is retried like a network error. */
21
+ export const EXTERNAL_INVOKE_TIMEOUT_MS = 30_000;
22
+ /**
23
+ * POST to a remote resource, retrying network errors, 429s, and 5xx responses up to
24
+ * `EXTERNAL_INVOKE_MAX_RETRIES` times with exponential backoff (500ms, 1s, 2s).
25
+ * Other 4xx responses are returned immediately since retrying won't change them.
26
+ * Throws the last network error if every attempt fails to connect.
27
+ */
28
+ async function fetchWithRetry(url, init) {
29
+ for (let attempt = 0;; attempt++) {
30
+ try {
31
+ const response = await fetch(url, { ...init(), signal: AbortSignal.timeout(EXTERNAL_INVOKE_TIMEOUT_MS) });
32
+ if (response.status !== 429 && response.status < 500) {
33
+ return response;
34
+ }
35
+ if (attempt >= EXTERNAL_INVOKE_MAX_RETRIES) {
36
+ return response;
37
+ }
38
+ }
39
+ catch (error) {
40
+ if (attempt >= EXTERNAL_INVOKE_MAX_RETRIES) {
41
+ throw error;
42
+ }
43
+ }
44
+ await new Promise(resolve => setTimeout(resolve, EXTERNAL_INVOKE_BASE_DELAY_MS * 2 ** attempt));
45
+ }
10
46
  }
11
47
  export class CombinationResourceInvoker {
12
48
  resourceRepository;
13
49
  costLimitLamports;
14
50
  publicKey;
15
51
  privateKey;
16
- constructor(resourceRepository, costLimitLamports, publicKey, privateKey) {
52
+ grantService;
53
+ /** Without a `grantService`, calls to restricted remote functions fail. */
54
+ constructor(resourceRepository, costLimitLamports, publicKey, privateKey, grantService) {
17
55
  this.resourceRepository = resourceRepository;
18
56
  this.costLimitLamports = costLimitLamports;
19
57
  this.publicKey = publicKey;
20
58
  this.privateKey = privateKey;
59
+ this.grantService = grantService;
21
60
  }
22
61
  /**
23
62
  * Invoke a function on a resource.
24
63
  * If the resource has a baseUrl, it will be invoked as a remote function.
64
+ * If the function is restricted, it attaches an authorization from a held grant.
25
65
  * If the resource has a paymentManifest, it will create a payment voucher and pass it to the resource.
26
66
  * Otherwise, it will be invoked as a local function.
27
67
  *
@@ -46,24 +86,36 @@ export class CombinationResourceInvoker {
46
86
  if (!resourceFunction) {
47
87
  throw new Error(`Function ${functionName} not found in resource ${resource.name}`);
48
88
  }
89
+ const target = { resourcePath: resource.path, resourceName: resource.name, functionName, parameters };
90
+ // Checked before the voucher, so a call this node can't make never signs one.
91
+ const chain = resourceFunction.restricted ? await this.grantService?.chainFor(url, target) : undefined;
92
+ if (resourceFunction.restricted && !chain) {
93
+ return { value: null, error: { message: `Failed to invoke ${resource.name}.${functionName}: it requires a grant from ${url}, and this node holds none that covers the call` } };
94
+ }
49
95
  let paymentVoucher;
50
96
  if (resourceFunction.paymentManifest) {
51
97
  console.log(`Resource function ${resource.name}.${functionName} has a payment manifest, creating payment voucher...`);
52
98
  if (resourceFunction.paymentManifest.costLamports > this.costLimitLamports) {
53
99
  throw new Error(`Function ${functionName} cost ${resourceFunction.paymentManifest.costLamports} lamports, which exceeds the cost limit of ${this.costLimitLamports} lamports`);
54
100
  }
55
- paymentVoucher = this.createPaymentVoucher(resourceFunction.paymentManifest.recipientAddress, resourceFunction.paymentManifest.costLamports, resource.path, resource.name, parameters);
101
+ paymentVoucher = this.createPaymentVoucher(resourceFunction.paymentManifest.recipientAddress, resourceFunction.paymentManifest.costLamports, resource.path, resource.name, functionName, parameters);
56
102
  requestBody.paymentVoucher = paymentVoucher;
57
103
  }
58
- const result = await fetch(`${url}/v1/run/resource`, {
104
+ // The voucher is created once and reused on every attempt, so the operator's
105
+ // requestId/invocationId check prevents a retried call from being settled twice.
106
+ // The authorization is signed per attempt, since the provider refuses a reused nonce.
107
+ const result = await fetchWithRetry(`${url}/v1/run/resource`, () => ({
59
108
  method: 'POST',
60
- body: JSON.stringify(requestBody),
109
+ body: JSON.stringify(chain && this.grantService ? { ...requestBody, authorization: this.grantService.authorize(chain, target) } : requestBody),
61
110
  headers: {
62
111
  'Content-Type': 'application/json',
63
112
  },
64
- });
113
+ }));
65
114
  if (!result.ok) {
66
- return { value: null, error: { message: `Failed to invoke ${resource.name}.${functionName}: ${result.statusText}` } };
115
+ // The provider's JSON error is more useful than the status text, which can be empty.
116
+ const detail = await result.json().then((body) => body?.error, () => undefined);
117
+ const reason = typeof detail === 'string' ? detail : `${result.status} ${result.statusText}`.trim();
118
+ return { value: null, error: { message: `Failed to invoke ${resource.name}.${functionName}: ${reason}` } };
67
119
  }
68
120
  const data = await result.json();
69
121
  return { value: data.value, error: data.error };
@@ -77,12 +129,13 @@ export class CombinationResourceInvoker {
77
129
  return { value: result, error: null };
78
130
  }
79
131
  }
80
- createPaymentVoucher(recipientAddress, amount, resourcePath, resourceName, parameters) {
132
+ createPaymentVoucher(recipientAddress, amount, resourcePath, resourceName, functionName, parameters) {
81
133
  const requestId = crypto.randomUUID();
82
134
  const invocationId = crypto.randomUUID();
83
135
  // Expire in 2 minutes
84
136
  const expiresAt = new Date(Date.now() + 1000 * 60 * 2).toISOString();
85
- const signature = signData(paymentVoucherPayload({ requestId, invocationId, amount, recipientAddress, expiresAt, publicKey: this.publicKey }, resourcePath, resourceName, parameters), this.privateKey);
86
- return { requestId, invocationId, amount, recipientAddress, signature, expiresAt, publicKey: this.publicKey };
137
+ const hash = invocationHash(resourcePath, resourceName, functionName, parameters);
138
+ const signature = signData(paymentVoucherPayload({ requestId, invocationId, amount, recipientAddress, expiresAt, invocationHash: hash, publicKey: this.publicKey }), this.privateKey);
139
+ return { requestId, invocationId, amount, recipientAddress, signature, expiresAt, invocationHash: hash, publicKey: this.publicKey };
87
140
  }
88
141
  }
@@ -1,6 +1,6 @@
1
1
  import { Resource, ResourceFunction, ResourceFunctionParameter } from "requestscript";
2
2
  import { Database } from "sqlite";
3
- import type { Peer } from "../peer/repository.js";
3
+ import type { StoredPeer } from "../peer/repository.js";
4
4
  export interface StellariumResourceFunctionPaymentManifest {
5
5
  id?: number;
6
6
  costLamports: number;
@@ -9,27 +9,66 @@ export interface StellariumResourceFunctionPaymentManifest {
9
9
  export interface StellariumResourceFunction extends ResourceFunction {
10
10
  id?: number;
11
11
  paymentManifest?: StellariumResourceFunctionPaymentManifest;
12
+ /** Callers need a grant from the hosting node. Announced to peers so callers can tell. */
13
+ restricted?: boolean;
12
14
  }
13
15
  export interface StellariumResource extends Resource {
14
16
  id: number;
15
17
  baseUrl: string;
16
18
  functions: StellariumResourceFunction[];
17
19
  }
20
+ /**
21
+ * The object a node signs to broadcast its resources. Key order is part of the signed bytes.
22
+ * Functions' `exec` implementations are dropped by JSON serialization on both ends.
23
+ */
24
+ export declare function resourceAnnouncementPayload(resources: StellariumResource[], expiry: string): {
25
+ resources: StellariumResource[];
26
+ expiry: string;
27
+ };
28
+ export type SaveAllResult = {
29
+ ok: true;
30
+ rejected: string[];
31
+ } | {
32
+ ok: false;
33
+ };
18
34
  export declare class ResourceRepository {
19
35
  private readonly db;
20
36
  private resources;
21
- constructor(db: Database, resources?: StellariumResource[]);
22
- create(resource: StellariumResource): Promise<void>;
23
- saveAll(resources: StellariumResource[], existingPeer: Peer, signature: string, expiry: string): Promise<boolean>;
24
- getResourceByName(name: string): Promise<StellariumResource | undefined>;
25
- getInternalResource(path: string, name: string): Promise<Resource | undefined>;
37
+ private readonly baseUrl;
38
+ private writes;
39
+ constructor(db: Database, resources?: StellariumResource[], baseUrl?: string);
40
+ private serialize;
41
+ private transaction;
42
+ private isInternal;
43
+ /**
44
+ * Saves a resource learned from the starting peer, if it isn't known yet. Resources hosted
45
+ * by this node and resources already known are left alone, so startup can run on every boot.
46
+ */
47
+ learn(resource: StellariumResource): Promise<void>;
48
+ /**
49
+ * Saves the resources a peer broadcast, after checking the peer signed them.
50
+ * A peer may only publish resources served from its own base URL, and may not replace a
51
+ * resource owned by another peer or hosted by this node. Those are returned in `rejected`.
52
+ * Accepted resources are replaced as a whole, so added or removed functions,
53
+ * parameters and payment manifests all take effect.
54
+ */
55
+ saveAll(resources: StellariumResource[], peer: StoredPeer, signature: string, expiry: string): Promise<SaveAllResult>;
56
+ private insert;
57
+ private insertFunctions;
58
+ private deleteFunctions;
59
+ getInternalResource(path: string, name: string): Promise<StellariumResource | undefined>;
26
60
  getResource(key: string): Promise<Resource | undefined>;
27
61
  getInternalResources(): Promise<StellariumResource[]>;
28
- getExternalResources(): Promise<StellariumResource[]>;
62
+ /** Learned resources in insertion order. A negative `limit` means no limit. */
63
+ getExternalResources(offset?: number, limit?: number): Promise<StellariumResource[]>;
29
64
  getResourceFunction(resourcePath: string, resourceName: string, functionName: string): Promise<StellariumResourceFunction | undefined>;
30
65
  getResourceFunctions(resourceId: number): Promise<StellariumResourceFunction[]>;
31
66
  getResourceParameters(resourceFunctionId: number): Promise<ResourceFunctionParameter[]>;
32
67
  getResourceFunctionPaymentManifest(resourceFunctionId: number): Promise<StellariumResourceFunctionPaymentManifest | undefined>;
33
- getAllResources(): Promise<StellariumResource[]>;
68
+ /**
69
+ * One page of every resource this node knows: the ones it hosts first, then learned ones.
70
+ * Both are in a fixed order, so offsets stay stable as resources are learned.
71
+ */
72
+ getResources(offset: number, limit: number): Promise<StellariumResource[]>;
34
73
  }
35
74
  //# sourceMappingURL=repository.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/resource/repository.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAClC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAElD,MAAM,WAAW,yCAAyC;IACtD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA2B,SAAQ,gBAAgB;IAChE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,eAAe,CAAC,EAAE,yCAAyC,CAAC;CAC/D;AAED,MAAM,WAAW,kBAAmB,SAAQ,QAAQ;IAChD,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,0BAA0B,EAAE,CAAC;CAC3C;AAED,qBAAa,kBAAkB;IACf,OAAO,CAAC,QAAQ,CAAC,EAAE;IAAY,OAAO,CAAC,SAAS;IAA5D,YAA6B,EAAE,EAAE,QAAQ,EAAU,SAAS,GAAE,kBAAkB,EAAO,EAAI;IAErF,MAAM,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmCxD;IAEK,OAAO,CAAC,SAAS,EAAE,kBAAkB,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAsDtH;IAEK,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAE7E;IAEK,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAEnF;IAEK,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CA0C5D;IAEK,oBAAoB,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAE1D;IAEK,oBAAoB,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAiB1D;IAEK,mBAAmB,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,GAAG,SAAS,CAAC,CAmB3I;IAEK,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,EAAE,CAAC,CAUpF;IAEK,qBAAqB,CAAC,kBAAkB,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,EAAE,CAAC,CAK5F;IAEK,kCAAkC,CAAC,kBAAkB,EAAE,MAAM,GAAG,OAAO,CAAC,yCAAyC,GAAG,SAAS,CAAC,CAUnI;IAEK,eAAe,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CASrD;CACJ"}
1
+ {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/resource/repository.ts"],"names":[],"mappings":"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"}
@@ -1,20 +1,108 @@
1
- import { verifyData } from "../crypto/utils.js";
1
+ import { isFreshExpiry, safeVerifyData } from "../crypto/utils.js";
2
+ /**
3
+ * The object a node signs to broadcast its resources. Key order is part of the signed bytes.
4
+ * Functions' `exec` implementations are dropped by JSON serialization on both ends.
5
+ */
6
+ export function resourceAnnouncementPayload(resources, expiry) {
7
+ return { resources, expiry };
8
+ }
9
+ const resourceKey = (resource) => `${resource.path}.${resource.name}`;
2
10
  export class ResourceRepository {
3
11
  db;
4
12
  resources;
5
- constructor(db, resources = []) {
13
+ baseUrl;
14
+ // The connection is shared, so transactions are serialized to keep them from nesting.
15
+ writes = Promise.resolve();
16
+ constructor(db, resources = [], baseUrl = '') {
6
17
  this.db = db;
7
18
  this.resources = resources;
19
+ this.baseUrl = baseUrl;
20
+ }
21
+ serialize(work) {
22
+ const run = this.writes.then(work, work);
23
+ this.writes = run.catch(() => undefined);
24
+ return run;
8
25
  }
9
- async create(resource) {
26
+ transaction(work) {
27
+ return this.serialize(async () => {
28
+ await this.db.exec('BEGIN IMMEDIATE');
29
+ try {
30
+ const result = await work();
31
+ await this.db.exec('COMMIT');
32
+ return result;
33
+ }
34
+ catch (error) {
35
+ await this.db.exec('ROLLBACK');
36
+ throw error;
37
+ }
38
+ });
39
+ }
40
+ isInternal(resource) {
41
+ return this.resources.some(r => r.path === resource.path && r.name === resource.name);
42
+ }
43
+ /**
44
+ * Saves a resource learned from the starting peer, if it isn't known yet. Resources hosted
45
+ * by this node and resources already known are left alone, so startup can run on every boot.
46
+ */
47
+ async learn(resource) {
48
+ if (this.isInternal(resource) || resource.baseUrl === this.baseUrl) {
49
+ return;
50
+ }
51
+ await this.transaction(async () => {
52
+ const existing = await this.db.get('SELECT id FROM resource WHERE path = ? AND name = ?', [resource.path, resource.name]);
53
+ if (!existing) {
54
+ await this.insert(resource, null);
55
+ }
56
+ });
57
+ }
58
+ /**
59
+ * Saves the resources a peer broadcast, after checking the peer signed them.
60
+ * A peer may only publish resources served from its own base URL, and may not replace a
61
+ * resource owned by another peer or hosted by this node. Those are returned in `rejected`.
62
+ * Accepted resources are replaced as a whole, so added or removed functions,
63
+ * parameters and payment manifests all take effect.
64
+ */
65
+ async saveAll(resources, peer, signature, expiry) {
66
+ if (!Array.isArray(resources) || !isFreshExpiry(expiry) || !safeVerifyData(resourceAnnouncementPayload(resources, expiry), peer.publicKey, signature)) {
67
+ return { ok: false };
68
+ }
69
+ const rejected = [];
70
+ await this.transaction(async () => {
71
+ for (const resource of resources) {
72
+ if (resource.baseUrl !== peer.baseUrl || this.isInternal(resource)) {
73
+ rejected.push(resourceKey(resource));
74
+ continue;
75
+ }
76
+ const existing = await this.db.get('SELECT * FROM resource WHERE path = ? AND name = ?', [resource.path, resource.name]);
77
+ if (!existing) {
78
+ await this.insert(resource, peer.id);
79
+ continue;
80
+ }
81
+ // Unowned rows came from the starting peer; the peer they point at may claim them.
82
+ const ownedByPeer = existing.peer_id === null ? existing.base_url === peer.baseUrl : existing.peer_id === peer.id;
83
+ if (!ownedByPeer) {
84
+ rejected.push(resourceKey(resource));
85
+ continue;
86
+ }
87
+ await this.deleteFunctions(existing.id);
88
+ await this.db.run('UPDATE resource SET base_url = ?, peer_id = ?, updated_at = ? WHERE id = ?', [resource.baseUrl, peer.id, new Date().toISOString(), existing.id]);
89
+ await this.insertFunctions(existing.id, resource.functions);
90
+ }
91
+ });
92
+ return { ok: true, rejected };
93
+ }
94
+ async insert(resource, peerId) {
10
95
  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) {
96
+ const result = await this.db.run('INSERT INTO resource (path, name, base_url, peer_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [resource.path, resource.name, resource.baseUrl, peerId, createdAt, createdAt]);
97
+ if (!result.changes || result.lastID === undefined) {
13
98
  throw new Error('Failed to create resource');
14
99
  }
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]);
100
+ await this.insertFunctions(result.lastID, resource.functions);
101
+ }
102
+ async insertFunctions(resourceId, functions) {
103
+ const createdAt = new Date().toISOString();
104
+ for (const fn of functions) {
105
+ const functionResult = await this.db.run('INSERT INTO resource_function (resource_id, name, return_type, restricted, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [resourceId, fn.name, fn.returnType, fn.restricted === true ? 1 : 0, createdAt, createdAt]);
18
106
  const functionId = functionResult.lastID;
19
107
  for (const parameter of fn.parameters) {
20
108
  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]);
@@ -24,45 +112,17 @@ export class ResourceRepository {
24
112
  }
25
113
  }
26
114
  }
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);
115
+ async deleteFunctions(resourceId) {
116
+ const functionIds = 'SELECT id FROM resource_function WHERE resource_id = ?';
117
+ await this.db.run(`DELETE FROM resource_function_payment_manifest WHERE resource_function_id IN (${functionIds})`, [resourceId]);
118
+ await this.db.run(`DELETE FROM resource_function_parameter WHERE resource_function_id IN (${functionIds})`, [resourceId]);
119
+ await this.db.run('DELETE FROM resource_function WHERE resource_id = ?', [resourceId]);
60
120
  }
61
121
  async getInternalResource(path, name) {
62
122
  return this.resources.find(r => r.path === path && r.name === name);
63
123
  }
64
124
  async getResource(key) {
65
- const resource = this.resources.find(r => `${r.path}.${r.name}` === key);
125
+ const resource = this.resources.find(r => resourceKey(r) === key);
66
126
  if (resource) {
67
127
  return resource;
68
128
  }
@@ -74,33 +134,22 @@ export class ResourceRepository {
74
134
  if (!result) {
75
135
  return undefined;
76
136
  }
77
- const resourceFunctions = await this.db.all('SELECT * FROM resource_function WHERE resource_id = ?', [result.id]);
78
137
  return {
79
138
  path: result.path,
80
139
  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
- }))),
140
+ // The CombinationResourceInvoker forwards calls on these functions to `metadata.baseUrl`.
141
+ functions: await this.getResourceFunctions(result.id),
90
142
  metadata: {
91
143
  baseUrl: result.base_url,
92
- paymentManifest: await this.getResourceFunctionPaymentManifest(result.id),
93
144
  }
94
145
  };
95
146
  }
96
147
  async getInternalResources() {
97
- return this.resources.map(r => ({ ...r, baseUrl: process.env.BASE_URL }));
148
+ return this.resources.map(r => ({ ...r, baseUrl: this.baseUrl }));
98
149
  }
99
- async getExternalResources() {
100
- const results = await this.db.all('SELECT * FROM resource');
101
- if (!results) {
102
- return [];
103
- }
150
+ /** Learned resources in insertion order. A negative `limit` means no limit. */
151
+ async getExternalResources(offset = 0, limit = -1) {
152
+ const results = await this.db.all('SELECT * FROM resource ORDER BY id LIMIT ? OFFSET ?', [limit, offset]);
104
153
  return Promise.all(results.map(async (r) => ({
105
154
  id: r.id,
106
155
  path: r.path,
@@ -126,6 +175,7 @@ export class ResourceRepository {
126
175
  parameters: await this.getResourceParameters(fn.id),
127
176
  exec: async () => undefined,
128
177
  paymentManifest: await this.getResourceFunctionPaymentManifest(fn.id),
178
+ restricted: fn.restricted === 1 || undefined,
129
179
  };
130
180
  }
131
181
  async getResourceFunctions(resourceId) {
@@ -137,10 +187,11 @@ export class ResourceRepository {
137
187
  parameters: await this.getResourceParameters(f.id),
138
188
  exec: async () => undefined,
139
189
  paymentManifest: await this.getResourceFunctionPaymentManifest(f.id),
190
+ restricted: f.restricted === 1 || undefined,
140
191
  })));
141
192
  }
142
193
  async getResourceParameters(resourceFunctionId) {
143
- return (await this.db.all('SELECT * FROM resource_function_parameter WHERE resource_function_id = ?', [resourceFunctionId])).map(p => ({
194
+ return (await this.db.all('SELECT * FROM resource_function_parameter WHERE resource_function_id = ? ORDER BY id', [resourceFunctionId])).map(p => ({
144
195
  name: p.name,
145
196
  type: p.parameter_type,
146
197
  }));
@@ -156,13 +207,17 @@ export class ResourceRepository {
156
207
  recipientAddress: result.recipient_address,
157
208
  };
158
209
  }
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 });
210
+ /**
211
+ * One page of every resource this node knows: the ones it hosts first, then learned ones.
212
+ * Both are in a fixed order, so offsets stay stable as resources are learned.
213
+ */
214
+ async getResources(offset, limit) {
215
+ const internal = (await this.getInternalResources()).slice(offset, offset + limit);
216
+ const remaining = limit - internal.length;
217
+ if (remaining <= 0) {
218
+ return internal;
164
219
  }
165
- resources.push(...externalResources);
166
- return resources;
220
+ const externalOffset = Math.max(0, offset - this.resources.length);
221
+ return [...internal, ...await this.getExternalResources(externalOffset, remaining)];
167
222
  }
168
223
  }
package/dist/routes.d.ts CHANGED
@@ -1,8 +1,16 @@
1
1
  import type { FastifyPluginAsync } from 'fastify';
2
2
  import type { Service } from './service.js';
3
+ export interface RateLimitConfig {
4
+ max: number;
5
+ /** Milliseconds, or a string such as '1 minute'. */
6
+ timeWindow: number | string;
7
+ }
3
8
  interface RouteOptions {
4
9
  service: Service;
10
+ rateLimit?: RateLimitConfig;
5
11
  }
12
+ export declare const DEFAULT_PAGE_SIZE = 100;
13
+ export declare const MAX_PAGE_SIZE = 500;
6
14
  /**
7
15
  * Encapsulates the routes
8
16
  * @param fastify Encapsulated Fastify Instance
@@ -1 +1 @@
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,CAmJ5C,CAAC;eAGa,MAAM"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAgC,MAAM,SAAS,CAAC;AAKhF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAK5C,MAAM,WAAW,eAAe;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,oDAAoD;IACpD,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;CAC/B;AAED,UAAU,YAAY;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,eAAe,CAAC;CAC/B;AAiDD,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,eAAO,MAAM,aAAa,MAAM,CAAC;AAsHjC;;;;GAIG;AACH,QAAA,MAAM,MAAM,EAAE,kBAAkB,CAAC,YAAY,CA8R5C,CAAC;eAGa,MAAM"}