stellarium-ts 0.12.0 → 0.14.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 +93 -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 +7 -2
  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 +56 -9
  29. package/dist/resource/repository.d.ts.map +1 -1
  30. package/dist/resource/repository.js +140 -68
  31. package/dist/routes.d.ts +8 -0
  32. package/dist/routes.d.ts.map +1 -1
  33. package/dist/routes.js +310 -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 +182 -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,4 +1,5 @@
1
1
  import { Resource, ResourceFunctionCallParameter, ResourceInvoker } from "requestscript";
2
+ import type { GrantService } from "../grant/service.js";
2
3
  import { ResourceRepository } from "./repository.js";
3
4
  export interface PaymentVoucher {
4
5
  requestId: string;
@@ -7,33 +8,44 @@ export interface PaymentVoucher {
7
8
  recipientAddress: string;
8
9
  signature: string;
9
10
  expiresAt: string;
11
+ invocationHash: string;
10
12
  publicKey: string;
11
13
  }
12
14
  /**
13
- * The object `createPaymentVoucher` signs. `signature` is excluded, and
14
- * `resourcePath`, `resourceName`, and `parameters` come from the invocation,
15
- * not from the voucher itself. Key order is part of the signed bytes.
15
+ * Hash binding a voucher to one function call. Providers recompute it from the
16
+ * request they receive and compare it to `voucher.invocationHash`.
17
+ * Key order is part of the hashed bytes.
16
18
  */
17
- export declare function paymentVoucherPayload(voucher: Omit<PaymentVoucher, "signature">, resourcePath: string, resourceName: string, parameters: ResourceFunctionCallParameter[]): {
19
+ export declare function invocationHash(resourcePath: string, resourceName: string, functionName: string, parameters: ResourceFunctionCallParameter[]): string;
20
+ /**
21
+ * The object `createPaymentVoucher` signs: every voucher field except `signature`.
22
+ * Key order is part of the signed bytes.
23
+ */
24
+ export declare function paymentVoucherPayload(voucher: Omit<PaymentVoucher, "signature">): {
18
25
  requestId: string;
19
26
  invocationId: string;
20
27
  amount: number;
21
28
  recipientAddress: string;
22
29
  expiresAt: string;
23
- resourcePath: string;
24
- resourceName: string;
25
- parameters: ResourceFunctionCallParameter[];
30
+ invocationHash: string;
26
31
  publicKey: string;
27
32
  };
33
+ export declare const EXTERNAL_INVOKE_MAX_RETRIES = 3;
34
+ export declare const EXTERNAL_INVOKE_BASE_DELAY_MS = 500;
35
+ /** Per attempt. A timed-out attempt is retried like a network error. */
36
+ export declare const EXTERNAL_INVOKE_TIMEOUT_MS = 30000;
28
37
  export declare class CombinationResourceInvoker implements ResourceInvoker {
29
38
  private readonly resourceRepository;
30
39
  private readonly costLimitLamports;
31
40
  private readonly publicKey;
32
41
  private readonly privateKey;
33
- constructor(resourceRepository: ResourceRepository, costLimitLamports: number, publicKey: string, privateKey: string);
42
+ private readonly grantService?;
43
+ /** Without a `grantService`, calls to restricted remote functions fail. */
44
+ constructor(resourceRepository: ResourceRepository, costLimitLamports: number, publicKey: string, privateKey: string, grantService?: GrantService | undefined);
34
45
  /**
35
46
  * Invoke a function on a resource.
36
47
  * If the resource has a baseUrl, it will be invoked as a remote function.
48
+ * If the function is restricted, it attaches an authorization from a held grant.
37
49
  * If the resource has a paymentManifest, it will create a payment voucher and pass it to the resource.
38
50
  * Otherwise, it will be invoked as a local function.
39
51
  *
@@ -48,6 +60,6 @@ export declare class CombinationResourceInvoker implements ResourceInvoker {
48
60
  message: string;
49
61
  } | null;
50
62
  }>;
51
- createPaymentVoucher(recipientAddress: string, amount: number, resourcePath: string, resourceName: string, parameters: ResourceFunctionCallParameter[]): PaymentVoucher;
63
+ createPaymentVoucher(recipientAddress: string, amount: number, resourcePath: string, resourceName: string, functionName: string, parameters: ResourceFunctionCallParameter[]): PaymentVoucher;
52
64
  }
53
65
  //# sourceMappingURL=invoker.d.ts.map
@@ -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,35 +1,82 @@
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;
7
7
  recipientAddress: string;
8
8
  }
9
+ export interface StellariumResourceFunctionParameter extends ResourceFunctionParameter {
10
+ description?: string;
11
+ }
9
12
  export interface StellariumResourceFunction extends ResourceFunction {
10
13
  id?: number;
14
+ description?: string;
15
+ parameters: StellariumResourceFunctionParameter[];
11
16
  paymentManifest?: StellariumResourceFunctionPaymentManifest;
17
+ /** Callers need a grant from the hosting node. Announced to peers so callers can tell. */
18
+ restricted?: boolean;
12
19
  }
13
20
  export interface StellariumResource extends Resource {
14
21
  id: number;
15
22
  baseUrl: string;
23
+ /** A URL where callers can learn more about the resource. */
24
+ website?: string;
25
+ description?: string;
16
26
  functions: StellariumResourceFunction[];
17
27
  }
28
+ /**
29
+ * The object a node signs to broadcast its resources. Key order is part of the signed bytes.
30
+ * Functions' `exec` implementations are dropped by JSON serialization on both ends.
31
+ */
32
+ export declare function resourceAnnouncementPayload(resources: StellariumResource[], expiry: string): {
33
+ resources: StellariumResource[];
34
+ expiry: string;
35
+ };
36
+ export type SaveAllResult = {
37
+ ok: true;
38
+ rejected: string[];
39
+ } | {
40
+ ok: false;
41
+ };
18
42
  export declare class ResourceRepository {
19
43
  private readonly db;
20
44
  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>;
45
+ private readonly baseUrl;
46
+ private writes;
47
+ constructor(db: Database, resources?: StellariumResource[], baseUrl?: string);
48
+ private serialize;
49
+ private transaction;
50
+ private isInternal;
51
+ /**
52
+ * Saves a resource learned from the starting peer, if it isn't known yet. Resources hosted
53
+ * by this node and resources already known are left alone, so startup can run on every boot.
54
+ */
55
+ learn(resource: StellariumResource): Promise<void>;
56
+ /**
57
+ * Saves the resources a peer broadcast, after checking the peer signed them.
58
+ * A peer may only publish resources served from its own base URL, and may not replace a
59
+ * resource owned by another peer or hosted by this node. Those are returned in `rejected`.
60
+ * Accepted resources are replaced as a whole, so added or removed functions,
61
+ * parameters and payment manifests all take effect.
62
+ */
63
+ saveAll(resources: StellariumResource[], peer: StoredPeer, signature: string, expiry: string): Promise<SaveAllResult>;
64
+ private insert;
65
+ private insertFunctions;
66
+ private deleteFunctions;
67
+ getInternalResource(path: string, name: string): Promise<StellariumResource | undefined>;
26
68
  getResource(key: string): Promise<Resource | undefined>;
27
69
  getInternalResources(): Promise<StellariumResource[]>;
28
- getExternalResources(): Promise<StellariumResource[]>;
70
+ /** Learned resources in insertion order. A negative `limit` means no limit. */
71
+ getExternalResources(offset?: number, limit?: number): Promise<StellariumResource[]>;
29
72
  getResourceFunction(resourcePath: string, resourceName: string, functionName: string): Promise<StellariumResourceFunction | undefined>;
30
73
  getResourceFunctions(resourceId: number): Promise<StellariumResourceFunction[]>;
31
- getResourceParameters(resourceFunctionId: number): Promise<ResourceFunctionParameter[]>;
74
+ getResourceParameters(resourceFunctionId: number): Promise<StellariumResourceFunctionParameter[]>;
32
75
  getResourceFunctionPaymentManifest(resourceFunctionId: number): Promise<StellariumResourceFunctionPaymentManifest | undefined>;
33
- getAllResources(): Promise<StellariumResource[]>;
76
+ /**
77
+ * One page of every resource this node knows: the ones it hosts first, then learned ones.
78
+ * Both are in a fixed order, so offsets stay stable as resources are learned.
79
+ */
80
+ getResources(offset: number, limit: number): Promise<StellariumResource[]>;
34
81
  }
35
82
  //# 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,68 +1,140 @@
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
+ /** Peers' resources aren't validated on receipt, so a description that isn't a string is stored as absent. */
10
+ function optionalText(value) {
11
+ return typeof value === 'string' ? value : null;
12
+ }
13
+ /** Only http(s) URLs are kept, since clients may render a resource's website as a link. */
14
+ function optionalWebsite(value) {
15
+ if (typeof value !== 'string' || !URL.canParse(value)) {
16
+ return null;
17
+ }
18
+ const { protocol } = new URL(value);
19
+ return protocol === 'http:' || protocol === 'https:' ? value : null;
20
+ }
21
+ const resourceKey = (resource) => `${resource.path}.${resource.name}`;
2
22
  export class ResourceRepository {
3
23
  db;
4
24
  resources;
5
- constructor(db, resources = []) {
25
+ baseUrl;
26
+ // The connection is shared, so transactions are serialized to keep them from nesting.
27
+ writes = Promise.resolve();
28
+ constructor(db, resources = [], baseUrl = '') {
6
29
  this.db = db;
7
30
  this.resources = resources;
31
+ this.baseUrl = baseUrl;
32
+ }
33
+ serialize(work) {
34
+ const run = this.writes.then(work, work);
35
+ this.writes = run.catch(() => undefined);
36
+ return run;
37
+ }
38
+ transaction(work) {
39
+ return this.serialize(async () => {
40
+ await this.db.exec('BEGIN IMMEDIATE');
41
+ try {
42
+ const result = await work();
43
+ await this.db.exec('COMMIT');
44
+ return result;
45
+ }
46
+ catch (error) {
47
+ await this.db.exec('ROLLBACK');
48
+ throw error;
49
+ }
50
+ });
51
+ }
52
+ isInternal(resource) {
53
+ return this.resources.some(r => r.path === resource.path && r.name === resource.name);
54
+ }
55
+ /**
56
+ * Saves a resource learned from the starting peer, if it isn't known yet. Resources hosted
57
+ * by this node and resources already known are left alone, so startup can run on every boot.
58
+ */
59
+ async learn(resource) {
60
+ if (this.isInternal(resource) || resource.baseUrl === this.baseUrl) {
61
+ return;
62
+ }
63
+ await this.transaction(async () => {
64
+ const existing = await this.db.get('SELECT id FROM resource WHERE path = ? AND name = ?', [resource.path, resource.name]);
65
+ if (!existing) {
66
+ await this.insert(resource, null);
67
+ }
68
+ });
69
+ }
70
+ /**
71
+ * Saves the resources a peer broadcast, after checking the peer signed them.
72
+ * A peer may only publish resources served from its own base URL, and may not replace a
73
+ * resource owned by another peer or hosted by this node. Those are returned in `rejected`.
74
+ * Accepted resources are replaced as a whole, so added or removed functions,
75
+ * parameters and payment manifests all take effect.
76
+ */
77
+ async saveAll(resources, peer, signature, expiry) {
78
+ if (!Array.isArray(resources) || !isFreshExpiry(expiry) || !safeVerifyData(resourceAnnouncementPayload(resources, expiry), peer.publicKey, signature)) {
79
+ return { ok: false };
80
+ }
81
+ const rejected = [];
82
+ await this.transaction(async () => {
83
+ for (const resource of resources) {
84
+ if (resource.baseUrl !== peer.baseUrl || this.isInternal(resource)) {
85
+ rejected.push(resourceKey(resource));
86
+ continue;
87
+ }
88
+ const existing = await this.db.get('SELECT * FROM resource WHERE path = ? AND name = ?', [resource.path, resource.name]);
89
+ if (!existing) {
90
+ await this.insert(resource, peer.id);
91
+ continue;
92
+ }
93
+ // Unowned rows came from the starting peer; the peer they point at may claim them.
94
+ const ownedByPeer = existing.peer_id === null ? existing.base_url === peer.baseUrl : existing.peer_id === peer.id;
95
+ if (!ownedByPeer) {
96
+ rejected.push(resourceKey(resource));
97
+ continue;
98
+ }
99
+ await this.deleteFunctions(existing.id);
100
+ await this.db.run('UPDATE resource SET base_url = ?, website = ?, description = ?, peer_id = ?, updated_at = ? WHERE id = ?', [resource.baseUrl, optionalWebsite(resource.website), optionalText(resource.description), peer.id, new Date().toISOString(), existing.id]);
101
+ await this.insertFunctions(existing.id, resource.functions);
102
+ }
103
+ });
104
+ return { ok: true, rejected };
8
105
  }
9
- async create(resource) {
106
+ async insert(resource, peerId) {
10
107
  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) {
108
+ const result = await this.db.run('INSERT INTO resource (path, name, base_url, website, description, peer_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [resource.path, resource.name, resource.baseUrl, optionalWebsite(resource.website), optionalText(resource.description), peerId, createdAt, createdAt]);
109
+ if (!result.changes || result.lastID === undefined) {
13
110
  throw new Error('Failed to create resource');
14
111
  }
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]);
112
+ await this.insertFunctions(result.lastID, resource.functions);
113
+ }
114
+ async insertFunctions(resourceId, functions) {
115
+ const createdAt = new Date().toISOString();
116
+ for (const fn of functions) {
117
+ const functionResult = await this.db.run('INSERT INTO resource_function (resource_id, name, return_type, description, restricted, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)', [resourceId, fn.name, fn.returnType, optionalText(fn.description), fn.restricted === true ? 1 : 0, createdAt, createdAt]);
18
118
  const functionId = functionResult.lastID;
19
119
  for (const parameter of fn.parameters) {
20
- await this.db.run('INSERT INTO resource_function_parameter (resource_function_id, name, parameter_type, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [functionId, parameter.name, parameter.type, createdAt, createdAt]);
120
+ await this.db.run('INSERT INTO resource_function_parameter (resource_function_id, name, parameter_type, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [functionId, parameter.name, parameter.type, optionalText(parameter.description), createdAt, createdAt]);
21
121
  }
22
122
  if (fn.paymentManifest) {
23
123
  await this.db.run('INSERT INTO resource_function_payment_manifest (resource_function_id, cost_lamports, recipient_address, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [functionId, fn.paymentManifest.costLamports, fn.paymentManifest.recipientAddress, createdAt, createdAt]);
24
124
  }
25
125
  }
26
126
  }
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);
127
+ async deleteFunctions(resourceId) {
128
+ const functionIds = 'SELECT id FROM resource_function WHERE resource_id = ?';
129
+ await this.db.run(`DELETE FROM resource_function_payment_manifest WHERE resource_function_id IN (${functionIds})`, [resourceId]);
130
+ await this.db.run(`DELETE FROM resource_function_parameter WHERE resource_function_id IN (${functionIds})`, [resourceId]);
131
+ await this.db.run('DELETE FROM resource_function WHERE resource_id = ?', [resourceId]);
60
132
  }
61
133
  async getInternalResource(path, name) {
62
134
  return this.resources.find(r => r.path === path && r.name === name);
63
135
  }
64
136
  async getResource(key) {
65
- const resource = this.resources.find(r => `${r.path}.${r.name}` === key);
137
+ const resource = this.resources.find(r => resourceKey(r) === key);
66
138
  if (resource) {
67
139
  return resource;
68
140
  }
@@ -74,38 +146,29 @@ export class ResourceRepository {
74
146
  if (!result) {
75
147
  return undefined;
76
148
  }
77
- const resourceFunctions = await this.db.all('SELECT * FROM resource_function WHERE resource_id = ?', [result.id]);
78
149
  return {
79
150
  path: result.path,
80
151
  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
- }))),
152
+ // The CombinationResourceInvoker forwards calls on these functions to `metadata.baseUrl`.
153
+ functions: await this.getResourceFunctions(result.id),
90
154
  metadata: {
91
155
  baseUrl: result.base_url,
92
- paymentManifest: await this.getResourceFunctionPaymentManifest(result.id),
93
156
  }
94
157
  };
95
158
  }
96
159
  async getInternalResources() {
97
- return this.resources.map(r => ({ ...r, baseUrl: process.env.BASE_URL }));
160
+ return this.resources.map(r => ({ ...r, baseUrl: this.baseUrl }));
98
161
  }
99
- async getExternalResources() {
100
- const results = await this.db.all('SELECT * FROM resource');
101
- if (!results) {
102
- return [];
103
- }
162
+ /** Learned resources in insertion order. A negative `limit` means no limit. */
163
+ async getExternalResources(offset = 0, limit = -1) {
164
+ const results = await this.db.all('SELECT * FROM resource ORDER BY id LIMIT ? OFFSET ?', [limit, offset]);
104
165
  return Promise.all(results.map(async (r) => ({
105
166
  id: r.id,
106
167
  path: r.path,
107
168
  name: r.name,
108
169
  baseUrl: r.base_url,
170
+ website: r.website ?? undefined,
171
+ description: r.description ?? undefined,
109
172
  metadata: { baseUrl: r.base_url },
110
173
  functions: await this.getResourceFunctions(r.id),
111
174
  })));
@@ -123,9 +186,11 @@ export class ResourceRepository {
123
186
  id: fn.id,
124
187
  name: fn.name,
125
188
  returnType: fn.return_type,
189
+ description: fn.description ?? undefined,
126
190
  parameters: await this.getResourceParameters(fn.id),
127
191
  exec: async () => undefined,
128
192
  paymentManifest: await this.getResourceFunctionPaymentManifest(fn.id),
193
+ restricted: fn.restricted === 1 || undefined,
129
194
  };
130
195
  }
131
196
  async getResourceFunctions(resourceId) {
@@ -134,15 +199,18 @@ export class ResourceRepository {
134
199
  id: f.id,
135
200
  name: f.name,
136
201
  returnType: f.return_type,
202
+ description: f.description ?? undefined,
137
203
  parameters: await this.getResourceParameters(f.id),
138
204
  exec: async () => undefined,
139
205
  paymentManifest: await this.getResourceFunctionPaymentManifest(f.id),
206
+ restricted: f.restricted === 1 || undefined,
140
207
  })));
141
208
  }
142
209
  async getResourceParameters(resourceFunctionId) {
143
- return (await this.db.all('SELECT * FROM resource_function_parameter WHERE resource_function_id = ?', [resourceFunctionId])).map(p => ({
210
+ return (await this.db.all('SELECT * FROM resource_function_parameter WHERE resource_function_id = ? ORDER BY id', [resourceFunctionId])).map(p => ({
144
211
  name: p.name,
145
212
  type: p.parameter_type,
213
+ description: p.description ?? undefined,
146
214
  }));
147
215
  }
148
216
  async getResourceFunctionPaymentManifest(resourceFunctionId) {
@@ -156,13 +224,17 @@ export class ResourceRepository {
156
224
  recipientAddress: result.recipient_address,
157
225
  };
158
226
  }
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 });
227
+ /**
228
+ * One page of every resource this node knows: the ones it hosts first, then learned ones.
229
+ * Both are in a fixed order, so offsets stay stable as resources are learned.
230
+ */
231
+ async getResources(offset, limit) {
232
+ const internal = (await this.getInternalResources()).slice(offset, offset + limit);
233
+ const remaining = limit - internal.length;
234
+ if (remaining <= 0) {
235
+ return internal;
164
236
  }
165
- resources.push(...externalResources);
166
- return resources;
237
+ const externalOffset = Math.max(0, offset - this.resources.length);
238
+ return [...internal, ...await this.getExternalResources(externalOffset, remaining)];
167
239
  }
168
240
  }
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,CAqS5C,CAAC;eAGa,MAAM"}