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.
- 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 +88 -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 +6 -1
- 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 +47 -8
- package/dist/resource/repository.d.ts.map +1 -1
- package/dist/resource/repository.js +122 -67
- package/dist/routes.d.ts +8 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +303 -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 +151 -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
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { isFreshExpiry, isValidPublicKey, safeVerifyData, signData } from "../crypto/utils.js";
|
|
2
|
+
import { invocationHash } from "../resource/invoker.js";
|
|
3
|
+
import { capabilityCovers, capabilityMatches, CLOCK_SKEW_TOLERANCE_MS, grantHash, grantRenewalPayload, grantRequestPayload, grantsHash, invocationAuthorizationPayload, isAuthorization, isCapability, isGrant, MAX_AUTHORIZATION_LIFETIME_MS, MAX_GRANT_CHAIN_LENGTH, MAX_GRANT_LIFETIME_MS, signGrant, verifyChain, } from "./grant.js";
|
|
4
|
+
/** Per grant request or renewal. */
|
|
5
|
+
export const GRANT_RENEW_TIMEOUT_MS = 10_000;
|
|
6
|
+
/**
|
|
7
|
+
* How long a held chain is kept after it expires while its issuer refuses to renew it. A refusal
|
|
8
|
+
* may be temporary, such as while this node's key is blocked, so chains aren't deleted at once.
|
|
9
|
+
*/
|
|
10
|
+
export const HELD_GRANT_RETENTION_MS = MAX_GRANT_LIFETIME_MS;
|
|
11
|
+
/** How often claiming a nonce also deletes expired ones. */
|
|
12
|
+
const NONCE_SWEEP_INTERVAL_MS = 60_000;
|
|
13
|
+
/** Most renewals `refreshDue` has in flight at once. */
|
|
14
|
+
const REFRESH_CONCURRENCY = 8;
|
|
15
|
+
/** Thrown by `block` for a key that isn't an Ed25519 SPKI key. */
|
|
16
|
+
export class InvalidPublicKeyError extends Error {
|
|
17
|
+
constructor() {
|
|
18
|
+
super('Invalid public key');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Another node answered a grant request or renewal with a refusal, as opposed to failing to answer. */
|
|
22
|
+
class GrantRefusedError extends Error {
|
|
23
|
+
}
|
|
24
|
+
const expired = (grant, now = Date.now()) => Date.parse(grant.expiresAt) <= now;
|
|
25
|
+
const leafOf = (chain) => chain[chain.length - 1];
|
|
26
|
+
const keyOf = (grant) => ({ issuer: grant.issuer, id: grant.id });
|
|
27
|
+
const heldKey = (chain) => keyOf(leafOf(chain));
|
|
28
|
+
/** The root grant's issuer, or `''` when the chain is malformed, so that `verifyChain` refuses it. */
|
|
29
|
+
const rootIssuer = (chain) => Array.isArray(chain) && isGrant(chain[0]) ? chain[0].issuer : '';
|
|
30
|
+
const isWellFormed = (chain) => Array.isArray(chain) && chain.length > 0 && chain.every(isGrant);
|
|
31
|
+
/** True when `renewed` holds the same grants as `chain`, position by position. Renewal keeps each grant's issuer and id. */
|
|
32
|
+
const sameGrants = (renewed, chain) => renewed.length === chain.length && renewed.every((grant, index) => grant.issuer === chain[index].issuer && grant.id === chain[index].id);
|
|
33
|
+
/**
|
|
34
|
+
* Issues, holds, renews and verifies capability grants. Every node plays all three roles:
|
|
35
|
+
* a provider issues root grants, a holder can delegate narrower grants and renew them for
|
|
36
|
+
* its own audiences, and renewal of either kind goes through `renew`.
|
|
37
|
+
*/
|
|
38
|
+
export class GrantService {
|
|
39
|
+
repository;
|
|
40
|
+
config;
|
|
41
|
+
approveGrant;
|
|
42
|
+
lastNonceSweep = 0;
|
|
43
|
+
/** Without `approveGrant`, this node refuses every grant request. */
|
|
44
|
+
constructor(repository, config, approveGrant) {
|
|
45
|
+
this.repository = repository;
|
|
46
|
+
this.config = config;
|
|
47
|
+
this.approveGrant = approveGrant;
|
|
48
|
+
}
|
|
49
|
+
get acceptsRequests() {
|
|
50
|
+
return this.approveGrant !== undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Issues a root grant for this node's resources. Returns it as a one-grant chain. */
|
|
53
|
+
async issue(audience, capabilities, options) {
|
|
54
|
+
checkCapabilities(capabilities);
|
|
55
|
+
checkOptions(options);
|
|
56
|
+
const grant = this.sign(crypto.randomUUID(), audience, capabilities, options.lifetimeMs, null, options.renewableUntil ?? null);
|
|
57
|
+
await this.repository.saveIssued({ id: grant.id, audience, capabilities, parent: null, lifetimeMs: options.lifetimeMs, renewableUntil: options.renewableUntil ?? null });
|
|
58
|
+
return [grant];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Narrows a grant this node holds for another audience. The child expires no later than the
|
|
62
|
+
* held grant. Name the held grant by its id, or by its issuer and id when two issuers used the same id.
|
|
63
|
+
*/
|
|
64
|
+
async delegate(heldGrant, audience, capabilities, options) {
|
|
65
|
+
checkCapabilities(capabilities);
|
|
66
|
+
checkOptions(options);
|
|
67
|
+
const chain = await this.findHeld(heldGrant);
|
|
68
|
+
if (expired(leafOf(chain))) {
|
|
69
|
+
throw new Error(`No unexpired held grant ${leafOf(chain).id}`);
|
|
70
|
+
}
|
|
71
|
+
if (chain.length >= MAX_GRANT_CHAIN_LENGTH) {
|
|
72
|
+
throw new Error(`Grant chains are limited to ${MAX_GRANT_CHAIN_LENGTH} grants`);
|
|
73
|
+
}
|
|
74
|
+
const parent = leafOf(chain);
|
|
75
|
+
if (!capabilities.every(child => parent.capabilities.some(capability => capabilityCovers(capability, child)))) {
|
|
76
|
+
throw new Error('A delegated grant may only narrow the grant it is delegated from');
|
|
77
|
+
}
|
|
78
|
+
const grant = this.sign(crypto.randomUUID(), audience, capabilities, options.lifetimeMs, parent, options.renewableUntil ?? null);
|
|
79
|
+
await this.repository.saveIssued({ id: grant.id, audience, capabilities, parent: heldKey(chain), lifetimeMs: options.lifetimeMs, renewableUntil: options.renewableUntil ?? null });
|
|
80
|
+
return [...chain, grant];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Revoked grants are refused in any chain this node verifies, and are never renewed. A
|
|
84
|
+
* delegator's revocation only stops renewal, since the provider never sees it, so it
|
|
85
|
+
* takes effect when the current copy expires. Name a grant this node issued by its id, and
|
|
86
|
+
* any other grant, such as one delegated further down a chain, by its issuer and id.
|
|
87
|
+
*/
|
|
88
|
+
async revoke(grant) {
|
|
89
|
+
await this.repository.revoke(typeof grant === 'string' ? { issuer: this.config.publicKey, id: grant } : grant);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Refuses `publicKey` everywhere this node can identify a caller: in any position of a
|
|
93
|
+
* restricted call's chain, and in renewals, grant requests and payment vouchers.
|
|
94
|
+
*/
|
|
95
|
+
async block(publicKey, reason) {
|
|
96
|
+
if (!isValidPublicKey(publicKey)) {
|
|
97
|
+
throw new InvalidPublicKeyError();
|
|
98
|
+
}
|
|
99
|
+
await this.repository.block(publicKey, reason ?? null);
|
|
100
|
+
}
|
|
101
|
+
/** False when the key wasn't blocked. */
|
|
102
|
+
async unblock(publicKey) {
|
|
103
|
+
return this.repository.unblock(publicKey);
|
|
104
|
+
}
|
|
105
|
+
async isBlocked(publicKey) {
|
|
106
|
+
return this.repository.anyBlocked([publicKey]);
|
|
107
|
+
}
|
|
108
|
+
async getBlocked(offset, limit) {
|
|
109
|
+
return this.repository.getBlocked(offset, limit);
|
|
110
|
+
}
|
|
111
|
+
/** Handles a holder's renewal request for a grant this node issued. Returns the fresh chain, or undefined if refused. */
|
|
112
|
+
async renew(request) {
|
|
113
|
+
// A replayed request only hands the audience another copy that it alone can use, so nonces aren't tracked.
|
|
114
|
+
if (!isFreshExpiry(request.expiresAt, MAX_AUTHORIZATION_LIFETIME_MS + CLOCK_SKEW_TOLERANCE_MS) || !safeVerifyData(grantRenewalPayload(request), request.publicKey, request.signature)) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
const issued = await this.repository.getIssued(request.grantId);
|
|
118
|
+
if (!issued || issued.audience !== request.publicKey || await this.isBlocked(request.publicKey)) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
if (issued.renewableUntil !== null && Date.parse(issued.renewableUntil) <= Date.now()) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
let chain = [];
|
|
125
|
+
if (issued.parent !== null) {
|
|
126
|
+
const held = await this.repository.getHeld(issued.parent);
|
|
127
|
+
if (!held || expired(leafOf(held))) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
chain = held;
|
|
131
|
+
}
|
|
132
|
+
if (await this.repository.anyRevoked([{ issuer: this.config.publicKey, id: issued.id }, ...chain.map(keyOf)])) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const parent = chain.length > 0 ? leafOf(chain) : null;
|
|
136
|
+
return [...chain, this.sign(issued.id, issued.audience, issued.capabilities, issued.lifetimeMs, parent, issued.renewableUntil)];
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Handles a caller's request for a new root grant: checks its signature and nonce, then
|
|
140
|
+
* asks the approval hook. Returns the issued chain, or undefined if refused. Throws if the
|
|
141
|
+
* hook throws or returns an invalid approval; the caller must then sign a new request.
|
|
142
|
+
*/
|
|
143
|
+
async handleRequest(request) {
|
|
144
|
+
if (!this.approveGrant) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
const { publicKey, capabilities, nonce, expiresAt, signature } = request;
|
|
148
|
+
const details = request.details ?? null;
|
|
149
|
+
if (!validCapabilities(capabilities)) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
if (details !== null && (typeof details !== 'object' || Array.isArray(details))) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
if (!isFreshExpiry(expiresAt, MAX_AUTHORIZATION_LIFETIME_MS + CLOCK_SKEW_TOLERANCE_MS) || !safeVerifyData(grantRequestPayload(request), publicKey, signature)) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
if (await this.isBlocked(publicKey)) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
// The hook may have side effects, such as using up an invite code, so it sees each request once.
|
|
162
|
+
if (!await this.claimNonce(`request:${publicKey}`, nonce, Date.parse(expiresAt))) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
const approval = await this.approveGrant({ publicKey, capabilities, details });
|
|
166
|
+
if (!approval) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
const granted = approval.capabilities ?? capabilities;
|
|
170
|
+
try {
|
|
171
|
+
checkCapabilities(granted);
|
|
172
|
+
checkOptions(approval);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
throw new Error(`approveGrant returned an invalid approval: ${error instanceof Error ? error.message : String(error)}`);
|
|
176
|
+
}
|
|
177
|
+
return this.issue(publicKey, granted, approval);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Asks the node at `providerBaseUrl` for a root grant and stores it. Throws if refused.
|
|
181
|
+
* The provider verifies the grant again on every call, so a grant from the wrong key only fails there.
|
|
182
|
+
*/
|
|
183
|
+
async request(providerBaseUrl, capabilities, details) {
|
|
184
|
+
const unsigned = { publicKey: this.config.publicKey, capabilities, details: details ?? null, nonce: crypto.randomUUID(), expiresAt: new Date(Date.now() + MAX_AUTHORIZATION_LIFETIME_MS).toISOString() };
|
|
185
|
+
const request = { ...unsigned, signature: signData(grantRequestPayload(unsigned), this.config.privateKey) };
|
|
186
|
+
const grants = await this.postForGrants(`${providerBaseUrl}/v1/grants/request`, request, `Grant request to ${providerBaseUrl}`);
|
|
187
|
+
const chain = verifyChain(grants, rootIssuer(grants));
|
|
188
|
+
if (!chain.ok || grants.length !== 1 || grants[0].issuerBaseUrl !== providerBaseUrl || chain.leaf.audience !== this.config.publicKey) {
|
|
189
|
+
throw new Error(`Grant request to ${providerBaseUrl} returned an invalid grant`);
|
|
190
|
+
}
|
|
191
|
+
await this.repository.saveHeld(grants);
|
|
192
|
+
return grants;
|
|
193
|
+
}
|
|
194
|
+
/** Stores a chain whose leaf names this node as its audience. Throws if the chain is invalid. */
|
|
195
|
+
async add(chain) {
|
|
196
|
+
const result = verifyChain(chain, rootIssuer(chain));
|
|
197
|
+
if (!result.ok) {
|
|
198
|
+
throw new Error(`Invalid grant chain: ${result.reason}`);
|
|
199
|
+
}
|
|
200
|
+
if (result.leaf.audience !== this.config.publicKey) {
|
|
201
|
+
throw new Error('Grant chain is not addressed to this node');
|
|
202
|
+
}
|
|
203
|
+
await this.repository.saveHeld(chain);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Stores a chain passed in the node's start options. A held copy of the same grant that
|
|
207
|
+
* expires no earlier is a renewal of it, so it is kept. Returns false, storing nothing, for
|
|
208
|
+
* an expired chain. Throws if the chain is otherwise invalid.
|
|
209
|
+
*/
|
|
210
|
+
async seed(chain) {
|
|
211
|
+
if (isWellFormed(chain)) {
|
|
212
|
+
const held = await this.repository.getHeld(heldKey(chain));
|
|
213
|
+
if (held && Date.parse(leafOf(held).expiresAt) >= Date.parse(leafOf(chain).expiresAt)) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
// No grant in a valid chain outlives its parent, so the leaf expires first.
|
|
217
|
+
if (expired(leafOf(chain))) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
await this.add(chain);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* A held chain, rooted at the node serving `rootBaseUrl`, that covers the call. When every
|
|
226
|
+
* such chain has expired, the latest-expiring one is renewed, once, before giving up, so a
|
|
227
|
+
* call waits on at most one renewal. Undefined when this node holds no usable grant.
|
|
228
|
+
*/
|
|
229
|
+
async chainFor(rootBaseUrl, target) {
|
|
230
|
+
const now = Date.now();
|
|
231
|
+
// The provider refuses a chain from a key it no longer uses, such as after a key rotation.
|
|
232
|
+
// When this node doesn't know the provider as a peer, chains from any root key are tried.
|
|
233
|
+
const rootKey = await this.repository.getPeerKey(rootBaseUrl);
|
|
234
|
+
const usable = (chain) => (rootKey === undefined || chain[0].issuer === rootKey)
|
|
235
|
+
&& leafOf(chain).capabilities.some(capability => capabilityMatches(capability, target.resourcePath, target.resourceName, target.functionName, target.parameters));
|
|
236
|
+
const chain = (await this.repository.getUnexpiredHeldByRoot(rootBaseUrl, now)).find(usable);
|
|
237
|
+
if (chain) {
|
|
238
|
+
return chain;
|
|
239
|
+
}
|
|
240
|
+
const latestExpired = (await this.repository.getExpiredHeldByRoot(rootBaseUrl, now)).find(usable);
|
|
241
|
+
if (!latestExpired) {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
return await this.renewHeld(latestExpired);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Builds the authorization sent with a call. Sign a new one per attempt: providers refuse a reused nonce. */
|
|
252
|
+
authorize(chain, target) {
|
|
253
|
+
const nonce = crypto.randomUUID();
|
|
254
|
+
const expiresAt = new Date(Date.now() + MAX_AUTHORIZATION_LIFETIME_MS).toISOString();
|
|
255
|
+
const hash = invocationHash(target.resourcePath, target.resourceName, target.functionName, target.parameters);
|
|
256
|
+
const signature = signData(invocationAuthorizationPayload({ invocationHash: hash, publicKey: this.config.publicKey, nonce, expiresAt, grantsHash: grantsHash(chain) }), this.config.privateKey);
|
|
257
|
+
return { publicKey: this.config.publicKey, grants: chain, nonce, expiresAt, signature };
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Renews every held chain past half its lifetime. A failed renewal keeps the current copy,
|
|
261
|
+
* unless the issuer refused it and the chain expired more than `HELD_GRANT_RETENTION_MS` ago.
|
|
262
|
+
*/
|
|
263
|
+
async refreshDue() {
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
const due = (await this.repository.getAllHeld()).filter(chain => {
|
|
266
|
+
const leaf = leafOf(chain);
|
|
267
|
+
const issuedAt = Date.parse(leaf.issuedAt);
|
|
268
|
+
return now >= issuedAt + (Date.parse(leaf.expiresAt) - issuedAt) / 2;
|
|
269
|
+
});
|
|
270
|
+
const outcomes = [];
|
|
271
|
+
const refresh = async (chain) => {
|
|
272
|
+
const leaf = leafOf(chain);
|
|
273
|
+
try {
|
|
274
|
+
await this.renewHeld(chain);
|
|
275
|
+
return { renewed: leaf.id };
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (error instanceof GrantRefusedError && expired(leaf, now - HELD_GRANT_RETENTION_MS)) {
|
|
279
|
+
await this.repository.deleteHeld(heldKey(chain));
|
|
280
|
+
return { removed: leaf.id };
|
|
281
|
+
}
|
|
282
|
+
return { failed: { id: leaf.id, error: error instanceof Error ? error.message : String(error) } };
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
let next = 0;
|
|
286
|
+
const worker = async () => {
|
|
287
|
+
while (next < due.length) {
|
|
288
|
+
const index = next++;
|
|
289
|
+
outcomes[index] = await refresh(due[index]);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
await Promise.all(Array.from({ length: Math.min(REFRESH_CONCURRENCY, due.length) }, worker));
|
|
293
|
+
const result = { renewed: [], failed: [], removed: [] };
|
|
294
|
+
for (const outcome of outcomes) {
|
|
295
|
+
if ('renewed' in outcome) {
|
|
296
|
+
result.renewed.push(outcome.renewed);
|
|
297
|
+
}
|
|
298
|
+
else if ('removed' in outcome) {
|
|
299
|
+
result.removed.push(outcome.removed);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
result.failed.push(outcome.failed);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* True when `authorization` lets its signer make exactly this call: the chain is rooted at
|
|
309
|
+
* this node, unrevoked, addressed to the signer and covers the call, and the nonce is new.
|
|
310
|
+
*/
|
|
311
|
+
async verifyInvocation(authorization, target) {
|
|
312
|
+
if (!isAuthorization(authorization) || !isFreshExpiry(authorization.expiresAt, MAX_AUTHORIZATION_LIFETIME_MS + CLOCK_SKEW_TOLERANCE_MS)) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
const chain = verifyChain(authorization.grants, this.config.publicKey);
|
|
316
|
+
if (!chain.ok || chain.leaf.audience !== authorization.publicKey) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
const hash = invocationHash(target.resourcePath, target.resourceName, target.functionName, target.parameters);
|
|
320
|
+
const payload = invocationAuthorizationPayload({ invocationHash: hash, publicKey: authorization.publicKey, nonce: authorization.nonce, expiresAt: authorization.expiresAt, grantsHash: grantsHash(authorization.grants) });
|
|
321
|
+
if (!safeVerifyData(payload, authorization.publicKey, authorization.signature)) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
if (!chain.leaf.capabilities.some(capability => capabilityMatches(capability, target.resourcePath, target.resourceName, target.functionName, target.parameters))) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
if (await this.repository.anyRevoked(authorization.grants.map(keyOf))) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
// Every audience in the chain, so blocking a delegator also cuts off what it handed on.
|
|
331
|
+
if (await this.repository.anyBlocked(authorization.grants.map(grant => grant.audience))) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
return this.claimNonce(authorization.publicKey, authorization.nonce, Date.parse(authorization.expiresAt));
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Nonces are kept in the node's database, so they survive a restart. Replicas of a node that
|
|
338
|
+
* don't share a database don't see each other's nonces.
|
|
339
|
+
*/
|
|
340
|
+
async claimNonce(publicKey, nonce, expiresAt) {
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
if (now - this.lastNonceSweep >= NONCE_SWEEP_INTERVAL_MS) {
|
|
343
|
+
this.lastNonceSweep = now;
|
|
344
|
+
await this.repository.deleteExpiredNonces(now);
|
|
345
|
+
}
|
|
346
|
+
return this.repository.claimNonce(`${publicKey}:${nonce}`, expiresAt);
|
|
347
|
+
}
|
|
348
|
+
async findHeld(heldGrant) {
|
|
349
|
+
if (typeof heldGrant !== 'string') {
|
|
350
|
+
const chain = await this.repository.getHeld(heldGrant);
|
|
351
|
+
if (!chain) {
|
|
352
|
+
throw new Error(`No held grant ${heldGrant.id} from ${heldGrant.issuer}`);
|
|
353
|
+
}
|
|
354
|
+
return chain;
|
|
355
|
+
}
|
|
356
|
+
const chains = await this.repository.getHeldById(heldGrant);
|
|
357
|
+
if (chains.length === 0) {
|
|
358
|
+
throw new Error(`No held grant ${heldGrant}`);
|
|
359
|
+
}
|
|
360
|
+
if (chains.length > 1) {
|
|
361
|
+
throw new Error(`Several issuers gave this node a grant with id ${heldGrant}: name the issuer too`);
|
|
362
|
+
}
|
|
363
|
+
return chains[0];
|
|
364
|
+
}
|
|
365
|
+
/** Asks the leaf grant's issuer for a fresh chain and stores it. Throws if refused or invalid. */
|
|
366
|
+
async renewHeld(chain) {
|
|
367
|
+
const leaf = leafOf(chain);
|
|
368
|
+
const unsigned = { grantId: leaf.id, publicKey: this.config.publicKey, nonce: crypto.randomUUID(), expiresAt: new Date(Date.now() + MAX_AUTHORIZATION_LIFETIME_MS).toISOString() };
|
|
369
|
+
const request = { ...unsigned, signature: signData(grantRenewalPayload(unsigned), this.config.privateKey) };
|
|
370
|
+
const grants = await this.postForGrants(`${leaf.issuerBaseUrl}/v1/grants/renew`, request, `Renewal of grant ${leaf.id}`);
|
|
371
|
+
// The issuer may not swap in a different grant, root or chain, so the renewal replaces the held copy.
|
|
372
|
+
const renewed = verifyChain(grants, chain[0].issuer);
|
|
373
|
+
if (!renewed.ok || !sameGrants(grants, chain) || renewed.leaf.audience !== this.config.publicKey) {
|
|
374
|
+
throw new Error(`Renewal of grant ${leaf.id} returned an invalid chain`);
|
|
375
|
+
}
|
|
376
|
+
await this.repository.saveHeld(grants);
|
|
377
|
+
return grants;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* POSTs a signed request to another node's grant endpoint and returns the chain it answered
|
|
381
|
+
* with, unverified. Throws `GrantRefusedError` if the node refused, and an Error if it failed.
|
|
382
|
+
*/
|
|
383
|
+
async postForGrants(url, body, description) {
|
|
384
|
+
const response = await fetch(url, {
|
|
385
|
+
method: 'POST',
|
|
386
|
+
body: JSON.stringify(body),
|
|
387
|
+
headers: { 'Content-Type': 'application/json' },
|
|
388
|
+
signal: AbortSignal.timeout(GRANT_RENEW_TIMEOUT_MS),
|
|
389
|
+
});
|
|
390
|
+
if (!response.ok) {
|
|
391
|
+
const detail = await response.json().then((answer) => answer?.error, () => undefined);
|
|
392
|
+
const reason = typeof detail === 'string' ? detail : response.status;
|
|
393
|
+
throw response.status === 403 ? new GrantRefusedError(`${description} refused: ${reason}`) : new Error(`${description} failed: ${reason}`);
|
|
394
|
+
}
|
|
395
|
+
const answer = await response.json();
|
|
396
|
+
return answer?.grants;
|
|
397
|
+
}
|
|
398
|
+
/** The copy expires after `lifetimeMs`, but no later than its parent or `renewableUntil`. */
|
|
399
|
+
sign(id, audience, capabilities, lifetimeMs, parent, renewableUntil) {
|
|
400
|
+
const now = Date.now();
|
|
401
|
+
const expiresAt = Math.min(now + lifetimeMs, parent ? Date.parse(parent.expiresAt) : Infinity, renewableUntil !== null ? Date.parse(renewableUntil) : Infinity);
|
|
402
|
+
return signGrant({
|
|
403
|
+
id,
|
|
404
|
+
issuer: this.config.publicKey,
|
|
405
|
+
issuerBaseUrl: this.config.baseUrl,
|
|
406
|
+
audience,
|
|
407
|
+
capabilities,
|
|
408
|
+
issuedAt: new Date(now).toISOString(),
|
|
409
|
+
expiresAt: new Date(expiresAt).toISOString(),
|
|
410
|
+
parent: parent ? grantHash(parent) : null,
|
|
411
|
+
}, this.config.privateKey);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/** Verifiers refuse a grant with no capabilities or a malformed one, so it is never signed. */
|
|
415
|
+
function validCapabilities(capabilities) {
|
|
416
|
+
return Array.isArray(capabilities) && capabilities.length > 0 && capabilities.every(isCapability);
|
|
417
|
+
}
|
|
418
|
+
function checkCapabilities(capabilities) {
|
|
419
|
+
if (!validCapabilities(capabilities)) {
|
|
420
|
+
throw new Error('capabilities must be a nonempty list of capabilities, each with a resourcePath');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function checkOptions(options) {
|
|
424
|
+
if (!Number.isSafeInteger(options.lifetimeMs) || options.lifetimeMs <= 0 || options.lifetimeMs > MAX_GRANT_LIFETIME_MS) {
|
|
425
|
+
throw new Error(`lifetimeMs must be a positive integer of at most ${MAX_GRANT_LIFETIME_MS}`);
|
|
426
|
+
}
|
|
427
|
+
// A grant expires by renewableUntil, so a past one would only sign an expired grant.
|
|
428
|
+
if (options.renewableUntil !== undefined && !(Date.parse(options.renewableUntil) > Date.now())) {
|
|
429
|
+
throw new Error('renewableUntil must be a future date');
|
|
430
|
+
}
|
|
431
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
export { StellariumNode, type StellariumNodeOptions } from './server.js';
|
|
1
|
+
export { StellariumNode, resolveConfig, type StellariumNodeOptions } from './server.js';
|
|
2
|
+
export type { NodeConfig } from './service.js';
|
|
2
3
|
export type { StellariumResource, StellariumResourceFunction, StellariumResourceFunctionPaymentManifest } from './resource/repository.js';
|
|
4
|
+
export { CombinationResourceInvoker, invocationHash, paymentVoucherPayload, type PaymentVoucher } from './resource/invoker.js';
|
|
5
|
+
export { grantHash, grantPayload, grantRenewalPayload, grantRequestPayload, grantsHash, CLOCK_SKEW_TOLERANCE_MS, invocationAuthorizationPayload, MAX_AUTHORIZATION_LIFETIME_MS, MAX_GRANT_CHAIN_LENGTH, MAX_GRANT_LIFETIME_MS, type Capability, type Grant, type GrantRenewalRequest, type GrantRequest, type InvocationAuthorization, } from './grant/grant.js';
|
|
6
|
+
export type { ApproveGrant, GrantApproval, GrantOptions } from './grant/service.js';
|
|
7
|
+
export type { GrantKey } from './grant/repository.js';
|
|
3
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACxF,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,YAAY,EAAE,kBAAkB,EAAE,0BAA0B,EAAE,yCAAyC,EAAE,MAAM,0BAA0B,CAAC;AAE1I,OAAO,EAAE,0BAA0B,EAAE,cAAc,EAAE,qBAAqB,EAAE,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE/H,OAAO,EACH,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,UAAU,EACV,uBAAuB,EACvB,8BAA8B,EAC9B,6BAA6B,EAC7B,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,UAAU,EACf,KAAK,KAAK,EACV,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,KAAK,uBAAuB,GAC/B,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACpF,YAAY,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
export { StellariumNode } from './server.js';
|
|
1
|
+
export { StellariumNode, resolveConfig } from './server.js';
|
|
2
|
+
// The voucher wire format, so the operator and other settlers can test against the real signer.
|
|
3
|
+
export { CombinationResourceInvoker, invocationHash, paymentVoucherPayload } from './resource/invoker.js';
|
|
4
|
+
// Grant wire format, so other implementations can test against the real signer.
|
|
5
|
+
export { grantHash, grantPayload, grantRenewalPayload, grantRequestPayload, grantsHash, CLOCK_SKEW_TOLERANCE_MS, invocationAuthorizationPayload, MAX_AUTHORIZATION_LIFETIME_MS, MAX_GRANT_CHAIN_LENGTH, MAX_GRANT_LIFETIME_MS, } from './grant/grant.js';
|
|
@@ -4,13 +4,44 @@ export interface Peer {
|
|
|
4
4
|
name: string;
|
|
5
5
|
publicKey: string;
|
|
6
6
|
}
|
|
7
|
+
export interface StoredPeer extends Peer {
|
|
8
|
+
id: number;
|
|
9
|
+
}
|
|
10
|
+
/** Why `announce` rejected a peer. */
|
|
11
|
+
export type PeerAnnouncementError = 'invalid_signature' | 'conflict';
|
|
12
|
+
/**
|
|
13
|
+
* The object a node signs to announce itself. Including `publicKey` binds the
|
|
14
|
+
* announcement to the key that signed it. Key order is part of the signed bytes.
|
|
15
|
+
*/
|
|
16
|
+
export declare function peerAnnouncementPayload(peer: Peer, expiry: string): {
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
name: string;
|
|
19
|
+
publicKey: string;
|
|
20
|
+
expiry: string;
|
|
21
|
+
};
|
|
7
22
|
export declare class PeerRepository {
|
|
8
23
|
private readonly db;
|
|
9
24
|
constructor(db: Database);
|
|
25
|
+
/**
|
|
26
|
+
* Saves a peer learned from a trusted source (the starting peer) without a signature.
|
|
27
|
+
* Does nothing if a peer with the same public key, base URL or name already exists.
|
|
28
|
+
*/
|
|
10
29
|
create(peer: Peer): Promise<void>;
|
|
11
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Saves a peer that announced itself. The announcement must be signed by `peer.publicKey`.
|
|
32
|
+
* A known peer (same public key) may change its base URL and name; a new peer may not
|
|
33
|
+
* take a base URL or name that another peer already holds.
|
|
34
|
+
*/
|
|
35
|
+
announce(peer: Peer, signature: string, expiry: string): Promise<{
|
|
36
|
+
ok: true;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
error: PeerAnnouncementError;
|
|
40
|
+
}>;
|
|
12
41
|
deleteAll(): Promise<void>;
|
|
13
42
|
getPeerList(): Promise<Peer[]>;
|
|
14
|
-
|
|
43
|
+
/** One page of peers, in insertion order so offsets stay stable as peers are added. */
|
|
44
|
+
getPeers(offset: number, limit: number): Promise<Peer[]>;
|
|
45
|
+
getPeerByPublicKey(publicKey: string): Promise<StoredPeer | undefined>;
|
|
15
46
|
}
|
|
16
47
|
//# sourceMappingURL=repository.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/peer/repository.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAElC,MAAM,WAAW,IAAI;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,cAAc;IACX,OAAO,CAAC,QAAQ,CAAC,EAAE;IAA/B,YAA6B,EAAE,EAAE,QAAQ,EAAI;
|
|
1
|
+
{"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/peer/repository.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAElC,MAAM,WAAW,IAAI;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAW,SAAQ,IAAI;IACpC,EAAE,EAAE,MAAM,CAAC;CACd;AAED,sCAAsC;AACtC,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,GAAG,UAAU,CAAC;AAErE;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM;;;;;EAGjE;AAaD,qBAAa,cAAc;IACX,OAAO,CAAC,QAAQ,CAAC,EAAE;IAA/B,YAA6B,EAAE,EAAE,QAAQ,EAAI;IAE7C;;;OAGG;IACG,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAMtC;IAED;;;;OAIG;IACG,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,qBAAqB,CAAA;KAAE,CAAC,CAoBjI;IAEK,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAE/B;IAEK,WAAW,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,CAQnC;IAED,uFAAuF;IACjF,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAQ7D;IAEK,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAG3E;CACJ"}
|
package/dist/peer/repository.js
CHANGED
|
@@ -1,29 +1,45 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isFreshExpiry, safeVerifyData } from "../crypto/utils.js";
|
|
2
|
+
/**
|
|
3
|
+
* The object a node signs to announce itself. Including `publicKey` binds the
|
|
4
|
+
* announcement to the key that signed it. Key order is part of the signed bytes.
|
|
5
|
+
*/
|
|
6
|
+
export function peerAnnouncementPayload(peer, expiry) {
|
|
7
|
+
const { baseUrl, name, publicKey } = peer;
|
|
8
|
+
return { baseUrl, name, publicKey, expiry };
|
|
9
|
+
}
|
|
10
|
+
function toPeer(row) {
|
|
11
|
+
return { id: row.id, baseUrl: row.base_url, name: row.name, publicKey: row.public_key };
|
|
12
|
+
}
|
|
2
13
|
export class PeerRepository {
|
|
3
14
|
db;
|
|
4
15
|
constructor(db) {
|
|
5
16
|
this.db = db;
|
|
6
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Saves a peer learned from a trusted source (the starting peer) without a signature.
|
|
20
|
+
* Does nothing if a peer with the same public key, base URL or name already exists.
|
|
21
|
+
*/
|
|
7
22
|
async create(peer) {
|
|
8
|
-
// Check if the peer already exists
|
|
9
|
-
const existingPeer = await this.db.get('SELECT * FROM peer WHERE base_url = ?', [peer.baseUrl]);
|
|
10
|
-
if (existingPeer) {
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
23
|
const createdAt = new Date().toISOString();
|
|
14
|
-
await this.db.run('INSERT INTO peer (base_url, name, public_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [peer.baseUrl, peer.name, peer.publicKey, createdAt, createdAt]);
|
|
24
|
+
await this.db.run('INSERT OR IGNORE INTO peer (base_url, name, public_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [peer.baseUrl, peer.name, peer.publicKey, createdAt, createdAt]);
|
|
15
25
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
return;
|
|
26
|
+
/**
|
|
27
|
+
* Saves a peer that announced itself. The announcement must be signed by `peer.publicKey`.
|
|
28
|
+
* A known peer (same public key) may change its base URL and name; a new peer may not
|
|
29
|
+
* take a base URL or name that another peer already holds.
|
|
30
|
+
*/
|
|
31
|
+
async announce(peer, signature, expiry) {
|
|
32
|
+
if (!isFreshExpiry(expiry) || !safeVerifyData(peerAnnouncementPayload(peer, expiry), peer.publicKey, signature)) {
|
|
33
|
+
return { ok: false, error: 'invalid_signature' };
|
|
25
34
|
}
|
|
26
|
-
await this.
|
|
35
|
+
const holder = await this.db.get('SELECT * FROM peer WHERE (base_url = ? OR name = ?) AND public_key != ?', [peer.baseUrl, peer.name, peer.publicKey]);
|
|
36
|
+
if (holder) {
|
|
37
|
+
return { ok: false, error: 'conflict' };
|
|
38
|
+
}
|
|
39
|
+
const now = new Date().toISOString();
|
|
40
|
+
await this.db.run(`INSERT INTO peer (base_url, name, public_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?)
|
|
41
|
+
ON CONFLICT (public_key) DO UPDATE SET base_url = excluded.base_url, name = excluded.name, updated_at = excluded.updated_at`, [peer.baseUrl, peer.name, peer.publicKey, now, now]);
|
|
42
|
+
return { ok: true };
|
|
27
43
|
}
|
|
28
44
|
async deleteAll() {
|
|
29
45
|
await this.db.run('DELETE FROM peer');
|
|
@@ -36,11 +52,17 @@ export class PeerRepository {
|
|
|
36
52
|
publicKey: result.public_key,
|
|
37
53
|
}));
|
|
38
54
|
}
|
|
55
|
+
/** One page of peers, in insertion order so offsets stay stable as peers are added. */
|
|
56
|
+
async getPeers(offset, limit) {
|
|
57
|
+
const results = await this.db.all('SELECT * FROM peer ORDER BY id LIMIT ? OFFSET ?', [limit, offset]);
|
|
58
|
+
return results.map(result => ({
|
|
59
|
+
baseUrl: result.base_url,
|
|
60
|
+
name: result.name,
|
|
61
|
+
publicKey: result.public_key,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
39
64
|
async getPeerByPublicKey(publicKey) {
|
|
40
65
|
const result = await this.db.get('SELECT * FROM peer WHERE public_key = ?', [publicKey]);
|
|
41
|
-
|
|
42
|
-
return result;
|
|
43
|
-
}
|
|
44
|
-
return undefined;
|
|
66
|
+
return result ? toPeer(result) : undefined;
|
|
45
67
|
}
|
|
46
68
|
}
|
|
@@ -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
|