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.
- package/dist/client/operatorClient.d.ts +10 -4
- package/dist/client/operatorClient.d.ts.map +1 -1
- package/dist/client/operatorClient.js +51 -7
- package/dist/crypto/utils.d.ts +9 -0
- package/dist/crypto/utils.d.ts.map +1 -1
- package/dist/crypto/utils.js +30 -1
- package/dist/db/db.d.ts +5 -4
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +93 -12
- package/dist/grant/grant.d.ts +132 -0
- package/dist/grant/grant.d.ts.map +1 -0
- package/dist/grant/grant.js +158 -0
- package/dist/grant/repository.d.ts +57 -0
- package/dist/grant/repository.d.ts.map +1 -0
- package/dist/grant/repository.js +105 -0
- package/dist/grant/service.d.ts +141 -0
- package/dist/grant/service.d.ts.map +1 -0
- package/dist/grant/service.js +431 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/peer/repository.d.ts +33 -2
- package/dist/peer/repository.d.ts.map +1 -1
- package/dist/peer/repository.js +43 -21
- package/dist/resource/invoker.d.ts +21 -9
- package/dist/resource/invoker.d.ts.map +1 -1
- package/dist/resource/invoker.js +69 -16
- package/dist/resource/repository.d.ts +56 -9
- package/dist/resource/repository.d.ts.map +1 -1
- package/dist/resource/repository.js +140 -68
- package/dist/routes.d.ts +8 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +310 -49
- package/dist/server.d.ts +62 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +119 -18
- package/dist/service.d.ts +24 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +9 -6
- package/dist/startup.d.ts +14 -0
- package/dist/startup.d.ts.map +1 -1
- package/dist/startup.js +90 -76
- package/dist/tests/client/operatorClient.test.d.ts +2 -0
- package/dist/tests/client/operatorClient.test.d.ts.map +1 -0
- package/dist/tests/client/operatorClient.test.js +96 -0
- package/dist/tests/db/db.test.d.ts +2 -0
- package/dist/tests/db/db.test.d.ts.map +1 -0
- package/dist/tests/db/db.test.js +64 -0
- package/dist/tests/grant/grant.test.d.ts +2 -0
- package/dist/tests/grant/grant.test.d.ts.map +1 -0
- package/dist/tests/grant/grant.test.js +133 -0
- package/dist/tests/grant/service.test.d.ts +2 -0
- package/dist/tests/grant/service.test.d.ts.map +1 -0
- package/dist/tests/grant/service.test.js +415 -0
- package/dist/tests/helpers.d.ts +13 -0
- package/dist/tests/helpers.d.ts.map +1 -0
- package/dist/tests/helpers.js +49 -0
- package/dist/tests/integration/network.test.d.ts +2 -0
- package/dist/tests/integration/network.test.d.ts.map +1 -0
- package/dist/tests/integration/network.test.js +188 -0
- package/dist/tests/peer/repository.test.d.ts +2 -0
- package/dist/tests/peer/repository.test.d.ts.map +1 -0
- package/dist/tests/peer/repository.test.js +50 -0
- package/dist/tests/resource/invoker.test.js +132 -5
- package/dist/tests/resource/repository.test.d.ts +2 -0
- package/dist/tests/resource/repository.test.d.ts.map +1 -0
- package/dist/tests/resource/repository.test.js +182 -0
- package/dist/tests/routes.test.d.ts +2 -0
- package/dist/tests/routes.test.d.ts.map +1 -0
- package/dist/tests/routes.test.js +565 -0
- package/dist/tests/server.test.d.ts +2 -0
- package/dist/tests/server.test.d.ts.map +1 -0
- package/dist/tests/server.test.js +37 -0
- package/dist/tests/startup.test.d.ts +2 -0
- package/dist/tests/startup.test.d.ts.map +1 -0
- package/dist/tests/startup.test.js +75 -0
- 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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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"}
|
package/dist/resource/invoker.js
CHANGED
|
@@ -1,27 +1,67 @@
|
|
|
1
|
-
import { signData } from "../crypto/utils.js";
|
|
1
|
+
import { hashData, signData } from "../crypto/utils.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
86
|
-
|
|
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 {
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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<
|
|
74
|
+
getResourceParameters(resourceFunctionId: number): Promise<StellariumResourceFunctionParameter[]>;
|
|
32
75
|
getResourceFunctionPaymentManifest(resourceFunctionId: number): Promise<StellariumResourceFunctionPaymentManifest | undefined>;
|
|
33
|
-
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
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 =>
|
|
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
|
-
|
|
82
|
-
|
|
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:
|
|
160
|
+
return this.resources.map(r => ({ ...r, baseUrl: this.baseUrl }));
|
|
98
161
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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.
|
|
166
|
-
return
|
|
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
|
package/dist/routes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"
|
|
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"}
|