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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grant.test.d.ts","sourceRoot":"","sources":["../../../src/tests/grant/grant.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { capabilityCovers, capabilityMatches, grantHash, grantPayload, MAX_GRANT_CHAIN_LENGTH, signGrant, verifyChain, } from "../../grant/grant.js";
|
|
3
|
+
import { verifyData } from "../../crypto/utils.js";
|
|
4
|
+
import { generateBase64KeyPair } from "../helpers.js";
|
|
5
|
+
const inHours = (hours) => new Date(Date.now() + hours * 3_600_000).toISOString();
|
|
6
|
+
const weather = { resourcePath: "com.example", resourceName: "Weather" };
|
|
7
|
+
// Shared, so a child computed a millisecond later doesn't outlive its parent.
|
|
8
|
+
const expiresAt = inHours(1);
|
|
9
|
+
function grantFrom(issuer, audience, capabilities, parent = null, overrides = {}) {
|
|
10
|
+
return signGrant({
|
|
11
|
+
id: crypto.randomUUID(),
|
|
12
|
+
issuer: issuer.publicKey,
|
|
13
|
+
issuerBaseUrl: "http://issuer",
|
|
14
|
+
audience: audience.publicKey,
|
|
15
|
+
capabilities,
|
|
16
|
+
issuedAt: new Date().toISOString(),
|
|
17
|
+
expiresAt,
|
|
18
|
+
parent: parent ? grantHash(parent) : null,
|
|
19
|
+
...overrides,
|
|
20
|
+
}, issuer.privateKey);
|
|
21
|
+
}
|
|
22
|
+
describe("verifyChain", () => {
|
|
23
|
+
const provider = generateBase64KeyPair();
|
|
24
|
+
const gateway = generateBase64KeyPair();
|
|
25
|
+
const user = generateBase64KeyPair();
|
|
26
|
+
const other = generateBase64KeyPair();
|
|
27
|
+
test("accepts a root grant and returns it as the leaf", () => {
|
|
28
|
+
const root = grantFrom(provider, gateway, [weather]);
|
|
29
|
+
expect(verifyChain([root], provider.publicKey)).toEqual({ ok: true, leaf: root });
|
|
30
|
+
expect(verifyData(grantPayload(root), provider.publicKey, root.signature)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
test("accepts a three-grant chain that narrows at each link", () => {
|
|
33
|
+
const root = grantFrom(provider, gateway, [{ resourcePath: "com.*" }]);
|
|
34
|
+
const middle = grantFrom(gateway, user, [weather], root);
|
|
35
|
+
const leaf = grantFrom(user, other, [{ ...weather, functionName: "temperature", constraints: { city: "Oslo" } }], middle);
|
|
36
|
+
expect(verifyChain([root, middle, leaf], provider.publicKey)).toEqual({ ok: true, leaf });
|
|
37
|
+
});
|
|
38
|
+
test.each([
|
|
39
|
+
["an empty chain", () => []],
|
|
40
|
+
["a non-array", () => "nope"],
|
|
41
|
+
["a malformed grant", () => [{ ...grantFrom(provider, gateway, [weather]), capabilities: [] }]],
|
|
42
|
+
])("rejects %s", (_, chain) => {
|
|
43
|
+
expect(verifyChain(chain(), provider.publicKey).ok).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
test("rejects a root issued by another key", () => {
|
|
46
|
+
expect(verifyChain([grantFrom(other, gateway, [weather])], provider.publicKey).ok).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
test("rejects a root that claims a parent", () => {
|
|
49
|
+
const root = grantFrom(provider, gateway, [weather], null, { parent: "something" });
|
|
50
|
+
expect(verifyChain([root], provider.publicKey).ok).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
test("rejects a tampered grant", () => {
|
|
53
|
+
const root = grantFrom(provider, gateway, [weather]);
|
|
54
|
+
expect(verifyChain([{ ...root, capabilities: [{ resourcePath: "*" }] }], provider.publicKey).ok).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
test("rejects an expired grant and one that lives too long", () => {
|
|
57
|
+
expect(verifyChain([grantFrom(provider, gateway, [weather], null, { expiresAt: inHours(-1) })], provider.publicKey).ok).toBe(false);
|
|
58
|
+
expect(verifyChain([grantFrom(provider, gateway, [weather], null, { expiresAt: inHours(24 * 31) })], provider.publicKey).ok).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
test("rejects a link signed by someone other than the parent's audience", () => {
|
|
61
|
+
const root = grantFrom(provider, gateway, [weather]);
|
|
62
|
+
expect(verifyChain([root, grantFrom(other, user, [weather], root)], provider.publicKey).ok).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
test("rejects a link naming a different parent", () => {
|
|
65
|
+
const root = grantFrom(provider, gateway, [weather]);
|
|
66
|
+
const sibling = grantFrom(provider, gateway, [weather]);
|
|
67
|
+
expect(verifyChain([root, grantFrom(gateway, user, [weather], sibling)], provider.publicKey).ok).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
test("rejects a link that outlives its parent", () => {
|
|
70
|
+
const root = grantFrom(provider, gateway, [weather]);
|
|
71
|
+
expect(verifyChain([root, grantFrom(gateway, user, [weather], root, { expiresAt: inHours(2) })], provider.publicKey).ok).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
test.each([
|
|
74
|
+
["a wider path", { resourcePath: "com.*" }],
|
|
75
|
+
["another resource", { resourcePath: "com.example", resourceName: "Traffic" }],
|
|
76
|
+
])("rejects a link that widens to %s", (_, capability) => {
|
|
77
|
+
const root = grantFrom(provider, gateway, [weather]);
|
|
78
|
+
expect(verifyChain([root, grantFrom(gateway, user, [capability], root)], provider.publicKey).ok).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
test("rejects a link that drops a constraint", () => {
|
|
81
|
+
const root = grantFrom(provider, gateway, [{ ...weather, constraints: { city: "Oslo" } }]);
|
|
82
|
+
expect(verifyChain([root, grantFrom(gateway, user, [weather], root)], provider.publicKey).ok).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
test("rejects a chain longer than the limit", () => {
|
|
85
|
+
const keys = Array.from({ length: MAX_GRANT_CHAIN_LENGTH + 1 }, () => generateBase64KeyPair());
|
|
86
|
+
const chain = [grantFrom(provider, keys[0], [weather])];
|
|
87
|
+
for (let i = 1; i <= MAX_GRANT_CHAIN_LENGTH; i++) {
|
|
88
|
+
chain.push(grantFrom(keys[i - 1], keys[i], [weather], chain[i - 1]));
|
|
89
|
+
}
|
|
90
|
+
expect(verifyChain(chain.slice(0, MAX_GRANT_CHAIN_LENGTH), provider.publicKey).ok).toBe(true);
|
|
91
|
+
expect(verifyChain(chain, provider.publicKey).ok).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe("capabilityMatches", () => {
|
|
95
|
+
const oslo = [{ name: "city", value: "Oslo" }];
|
|
96
|
+
test.each([
|
|
97
|
+
[{ resourcePath: "com.example" }, "com.example", true],
|
|
98
|
+
[{ resourcePath: "com.example.*" }, "com.example", true],
|
|
99
|
+
[{ resourcePath: "com.example.*" }, "com.example.sub", true],
|
|
100
|
+
[{ resourcePath: "com.example.*" }, "com.examplefoo", false],
|
|
101
|
+
[{ resourcePath: "com.*" }, "com.example", true],
|
|
102
|
+
[{ resourcePath: "*" }, "org.anything", true],
|
|
103
|
+
[{ resourcePath: "com.example" }, "com.example.sub", false],
|
|
104
|
+
])("%o on path %s is %s", (capability, path, expected) => {
|
|
105
|
+
expect(capabilityMatches(capability, path, "Weather", "temperature", oslo)).toBe(expected);
|
|
106
|
+
});
|
|
107
|
+
test("checks names and constraints", () => {
|
|
108
|
+
expect(capabilityMatches({ ...weather, functionName: "temperature" }, "com.example", "Weather", "humidity", oslo)).toBe(false);
|
|
109
|
+
expect(capabilityMatches({ ...weather, constraints: { city: "Oslo" } }, "com.example", "Weather", "temperature", oslo)).toBe(true);
|
|
110
|
+
expect(capabilityMatches({ ...weather, constraints: { city: "Paris" } }, "com.example", "Weather", "temperature", oslo)).toBe(false);
|
|
111
|
+
expect(capabilityMatches({ ...weather, constraints: { units: "C" } }, "com.example", "Weather", "temperature", oslo)).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
test("refuses a constrained parameter that appears more than once", () => {
|
|
114
|
+
const duplicated = [...oslo, { name: "city", value: "Paris" }];
|
|
115
|
+
expect(capabilityMatches({ ...weather, constraints: { city: "Oslo" } }, "com.example", "Weather", "temperature", duplicated)).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe("capabilityCovers", () => {
|
|
119
|
+
test.each([
|
|
120
|
+
[{ resourcePath: "*" }, { resourcePath: "*" }, true],
|
|
121
|
+
[{ resourcePath: "*" }, { resourcePath: "com.example" }, true],
|
|
122
|
+
[{ resourcePath: "com.*" }, { resourcePath: "com.example.*" }, true],
|
|
123
|
+
[{ resourcePath: "com.*" }, { resourcePath: "*" }, false],
|
|
124
|
+
[{ resourcePath: "com.example.*" }, { resourcePath: "com.examplefoo" }, false],
|
|
125
|
+
[{ resourcePath: "com.example" }, { resourcePath: "com.example.*" }, false],
|
|
126
|
+
[weather, { ...weather, functionName: "temperature" }, true],
|
|
127
|
+
[{ ...weather, functionName: "temperature" }, weather, false],
|
|
128
|
+
[{ ...weather, constraints: { city: "Oslo" } }, { ...weather, constraints: { city: "Oslo", units: "C" } }, true],
|
|
129
|
+
[{ ...weather, constraints: { city: "Oslo" } }, { ...weather, constraints: { city: "Paris" } }, false],
|
|
130
|
+
])("%o covers %o: %s", (parent, child, expected) => {
|
|
131
|
+
expect(capabilityCovers(parent, child)).toBe(expected);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.test.d.ts","sourceRoot":"","sources":["../../../src/tests/grant/service.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { openDatabase } from "../../db/db.js";
|
|
3
|
+
import { grantHash, grantRequestPayload, signGrant } from "../../grant/grant.js";
|
|
4
|
+
import { signData } from "../../crypto/utils.js";
|
|
5
|
+
import { GrantRepository } from "../../grant/repository.js";
|
|
6
|
+
import { PeerRepository } from "../../peer/repository.js";
|
|
7
|
+
import { GrantService, HELD_GRANT_RETENTION_MS } from "../../grant/service.js";
|
|
8
|
+
import { generateBase64KeyPair } from "../helpers.js";
|
|
9
|
+
const HOUR = 3_600_000;
|
|
10
|
+
const weather = { resourcePath: "com.example", resourceName: "Weather" };
|
|
11
|
+
const target = { resourcePath: "com.example", resourceName: "Weather", functionName: "temperature", parameters: [{ name: "city", value: "Oslo" }] };
|
|
12
|
+
describe("GrantService", () => {
|
|
13
|
+
const databases = [];
|
|
14
|
+
const nodes = new Map();
|
|
15
|
+
async function node(baseUrl, approveGrant) {
|
|
16
|
+
const db = await openDatabase(":memory:");
|
|
17
|
+
databases.push(db);
|
|
18
|
+
const keys = generateBase64KeyPair();
|
|
19
|
+
const service = new GrantService(new GrantRepository(db), { baseUrl, ...keys }, approveGrant);
|
|
20
|
+
nodes.set(baseUrl, service);
|
|
21
|
+
return { service, db, ...keys };
|
|
22
|
+
}
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
// Grant requests and renewals reach the issuing node's service directly.
|
|
25
|
+
vi.stubGlobal("fetch", vi.fn(async (url, init) => {
|
|
26
|
+
const body = JSON.parse(init.body);
|
|
27
|
+
const requesting = url.endsWith("/v1/grants/request");
|
|
28
|
+
const issuer = nodes.get(url.replace(/\/v1\/grants\/(renew|request)$/, ""));
|
|
29
|
+
const grants = requesting ? await issuer?.handleRequest(body) : await issuer?.renew(body);
|
|
30
|
+
return grants ? new Response(JSON.stringify({ grants }), { status: 200 }) : new Response(null, { status: 403 });
|
|
31
|
+
}));
|
|
32
|
+
});
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
vi.useRealTimers();
|
|
35
|
+
vi.unstubAllGlobals();
|
|
36
|
+
nodes.clear();
|
|
37
|
+
await Promise.all(databases.splice(0).map(db => db.close()));
|
|
38
|
+
});
|
|
39
|
+
test("a provider's grant lets its holder make a covered call", async () => {
|
|
40
|
+
const provider = await node("http://provider");
|
|
41
|
+
const caller = await node("http://caller");
|
|
42
|
+
await caller.service.add(await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR }));
|
|
43
|
+
const chain = await caller.service.chainFor("http://provider", target);
|
|
44
|
+
expect(chain).toHaveLength(1);
|
|
45
|
+
const authorization = caller.service.authorize(chain, target);
|
|
46
|
+
expect(await provider.service.verifyInvocation(authorization, target)).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
test("refuses a reused nonce, another call, another signer and a revoked grant", async () => {
|
|
49
|
+
const provider = await node("http://provider");
|
|
50
|
+
const caller = await node("http://caller");
|
|
51
|
+
const [grant] = await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR });
|
|
52
|
+
const authorization = caller.service.authorize([grant], target);
|
|
53
|
+
expect(await provider.service.verifyInvocation(authorization, target)).toBe(true);
|
|
54
|
+
expect(await provider.service.verifyInvocation(authorization, target)).toBe(false);
|
|
55
|
+
const otherCall = { ...target, parameters: [{ name: "city", value: "Paris" }] };
|
|
56
|
+
expect(await provider.service.verifyInvocation(caller.service.authorize([grant], target), otherCall)).toBe(false);
|
|
57
|
+
const thief = await node("http://thief");
|
|
58
|
+
expect(await provider.service.verifyInvocation(thief.service.authorize([grant], target), target)).toBe(false);
|
|
59
|
+
await provider.service.revoke(grant.id);
|
|
60
|
+
expect(await provider.service.verifyInvocation(caller.service.authorize([grant], target), target)).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
test("refuses a chain rooted at another node and a call outside the grant", async () => {
|
|
63
|
+
const provider = await node("http://provider");
|
|
64
|
+
const impostor = await node("http://impostor");
|
|
65
|
+
const caller = await node("http://caller");
|
|
66
|
+
const forged = await impostor.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR });
|
|
67
|
+
expect(await provider.service.verifyInvocation(caller.service.authorize(forged, target), target)).toBe(false);
|
|
68
|
+
const narrow = await provider.service.issue(caller.publicKey, [{ ...weather, functionName: "humidity" }], { lifetimeMs: HOUR });
|
|
69
|
+
expect(await provider.service.verifyInvocation(caller.service.authorize(narrow, target), target)).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
test("add refuses a chain addressed to another node or with a bad signature", async () => {
|
|
72
|
+
const provider = await node("http://provider");
|
|
73
|
+
const caller = await node("http://caller");
|
|
74
|
+
const other = await node("http://other");
|
|
75
|
+
await expect(caller.service.add(await provider.service.issue(other.publicKey, [weather], { lifetimeMs: HOUR }))).rejects.toThrow("not addressed");
|
|
76
|
+
const [grant] = await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR });
|
|
77
|
+
await expect(caller.service.add([{ ...grant, capabilities: [{ resourcePath: "*" }] }])).rejects.toThrow("Invalid grant chain");
|
|
78
|
+
});
|
|
79
|
+
test("delegates a narrower grant that the provider accepts", async () => {
|
|
80
|
+
const provider = await node("http://provider");
|
|
81
|
+
const gateway = await node("http://gateway");
|
|
82
|
+
const user = await node("http://user");
|
|
83
|
+
const [root] = await provider.service.issue(gateway.publicKey, [{ resourcePath: "com.*" }], { lifetimeMs: HOUR });
|
|
84
|
+
await gateway.service.add([root]);
|
|
85
|
+
await expect(gateway.service.delegate(root.id, user.publicKey, [{ resourcePath: "org.*" }], { lifetimeMs: HOUR })).rejects.toThrow("narrow");
|
|
86
|
+
const chain = await gateway.service.delegate(root.id, user.publicKey, [weather], { lifetimeMs: 2 * HOUR });
|
|
87
|
+
expect(chain).toHaveLength(2);
|
|
88
|
+
// Capped at the parent's expiry.
|
|
89
|
+
expect(chain[1].expiresAt).toBe(root.expiresAt);
|
|
90
|
+
await user.service.add(chain);
|
|
91
|
+
const held = await user.service.chainFor("http://provider", target);
|
|
92
|
+
expect(await provider.service.verifyInvocation(user.service.authorize(held, target), target)).toBe(true);
|
|
93
|
+
// Revoking the root at the provider cuts off everything delegated from it.
|
|
94
|
+
await provider.service.revoke(root.id);
|
|
95
|
+
expect(await provider.service.verifyInvocation(user.service.authorize(held, target), target)).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
test("renewal keeps the grant id, and is refused once revoked or past renewableUntil", async () => {
|
|
98
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
99
|
+
const provider = await node("http://provider");
|
|
100
|
+
const caller = await node("http://caller");
|
|
101
|
+
const [grant] = await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR, renewableUntil: new Date(Date.now() + 3 * HOUR).toISOString() });
|
|
102
|
+
await caller.service.add([grant]);
|
|
103
|
+
vi.setSystemTime(Date.now() + 0.4 * HOUR);
|
|
104
|
+
expect(await caller.service.refreshDue()).toEqual({ renewed: [], failed: [], removed: [] });
|
|
105
|
+
vi.setSystemTime(Date.now() + 0.2 * HOUR);
|
|
106
|
+
expect(await caller.service.refreshDue()).toEqual({ renewed: [grant.id], failed: [], removed: [] });
|
|
107
|
+
const [renewed] = (await caller.service.chainFor("http://provider", target));
|
|
108
|
+
expect(renewed.id).toBe(grant.id);
|
|
109
|
+
expect(Date.parse(renewed.expiresAt)).toBeGreaterThan(Date.parse(grant.expiresAt));
|
|
110
|
+
vi.setSystemTime(Date.now() + 3 * HOUR);
|
|
111
|
+
const { failed } = await caller.service.refreshDue();
|
|
112
|
+
expect(failed).toEqual([{ id: grant.id, error: expect.stringContaining("refused") }]);
|
|
113
|
+
const [revocable] = await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR });
|
|
114
|
+
await caller.service.add([revocable]);
|
|
115
|
+
await provider.service.revoke(revocable.id);
|
|
116
|
+
vi.setSystemTime(Date.now() + 0.6 * HOUR);
|
|
117
|
+
expect((await caller.service.refreshDue()).failed.map(f => f.id)).toContain(revocable.id);
|
|
118
|
+
});
|
|
119
|
+
test("deletes a chain that is still refused long after it expired", async () => {
|
|
120
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
121
|
+
const provider = await node("http://provider");
|
|
122
|
+
const caller = await node("http://caller");
|
|
123
|
+
const [grant] = await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR });
|
|
124
|
+
await caller.service.add([grant]);
|
|
125
|
+
await provider.service.revoke(grant.id);
|
|
126
|
+
vi.setSystemTime(Date.now() + 2 * HOUR);
|
|
127
|
+
expect((await caller.service.refreshDue()).removed).toEqual([]);
|
|
128
|
+
vi.setSystemTime(Date.now() + HELD_GRANT_RETENTION_MS);
|
|
129
|
+
expect(await caller.service.refreshDue()).toEqual({ renewed: [], failed: [], removed: [grant.id] });
|
|
130
|
+
expect(await caller.service.refreshDue()).toEqual({ renewed: [], failed: [], removed: [] });
|
|
131
|
+
});
|
|
132
|
+
test("an expired chain is renewed when it is needed", async () => {
|
|
133
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
134
|
+
const provider = await node("http://provider");
|
|
135
|
+
const caller = await node("http://caller");
|
|
136
|
+
await caller.service.add(await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR }));
|
|
137
|
+
vi.setSystemTime(Date.now() + 2 * HOUR);
|
|
138
|
+
const chain = await caller.service.chainFor("http://provider", target);
|
|
139
|
+
expect(chain).toHaveLength(1);
|
|
140
|
+
expect(Date.parse(chain[0].expiresAt)).toBeGreaterThan(Date.now());
|
|
141
|
+
expect(fetch).toHaveBeenCalledOnce();
|
|
142
|
+
});
|
|
143
|
+
test("a delegated grant is renewed against the delegator's current parent", async () => {
|
|
144
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
145
|
+
const provider = await node("http://provider");
|
|
146
|
+
const gateway = await node("http://gateway");
|
|
147
|
+
const user = await node("http://user");
|
|
148
|
+
const [root] = await provider.service.issue(gateway.publicKey, [weather], { lifetimeMs: HOUR });
|
|
149
|
+
await gateway.service.add([root]);
|
|
150
|
+
await user.service.add(await gateway.service.delegate(root.id, user.publicKey, [weather], { lifetimeMs: HOUR }));
|
|
151
|
+
vi.setSystemTime(Date.now() + 0.6 * HOUR);
|
|
152
|
+
expect((await gateway.service.refreshDue()).renewed).toEqual([root.id]);
|
|
153
|
+
expect((await user.service.refreshDue()).failed).toEqual([]);
|
|
154
|
+
const chain = (await user.service.chainFor("http://provider", target));
|
|
155
|
+
const freshRoot = (await gateway.service.chainFor("http://provider", target))[0];
|
|
156
|
+
expect(chain[0]).toEqual(freshRoot);
|
|
157
|
+
expect(await provider.service.verifyInvocation(user.service.authorize(chain, target), target)).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
test("accepts an authorization from a caller whose clock runs a little ahead", async () => {
|
|
160
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
161
|
+
const provider = await node("http://provider");
|
|
162
|
+
const caller = await node("http://caller");
|
|
163
|
+
const [grant] = await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR });
|
|
164
|
+
vi.setSystemTime(Date.now() + 10_000);
|
|
165
|
+
const authorization = caller.service.authorize([grant], target);
|
|
166
|
+
vi.setSystemTime(Date.now() - 10_000);
|
|
167
|
+
expect(await provider.service.verifyInvocation(authorization, target)).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
test("remembers accepted nonces across a restart", async () => {
|
|
170
|
+
const provider = await node("http://provider");
|
|
171
|
+
const caller = await node("http://caller");
|
|
172
|
+
const [grant] = await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR });
|
|
173
|
+
const authorization = caller.service.authorize([grant], target);
|
|
174
|
+
expect(await provider.service.verifyInvocation(authorization, target)).toBe(true);
|
|
175
|
+
const restarted = new GrantService(new GrantRepository(databases[0]), { baseUrl: "http://provider", publicKey: provider.publicKey, privateKey: provider.privateKey });
|
|
176
|
+
expect(await restarted.verifyInvocation(authorization, target)).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
test("keeps chains from different issuers that reuse a grant id", async () => {
|
|
179
|
+
const provider = await node("http://provider");
|
|
180
|
+
const other = await node("http://other");
|
|
181
|
+
const caller = await node("http://caller");
|
|
182
|
+
const [grant] = await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR });
|
|
183
|
+
const colliding = { ...grant, issuer: other.publicKey, issuerBaseUrl: "http://other" };
|
|
184
|
+
const forged = signGrant(colliding, other.privateKey);
|
|
185
|
+
await caller.service.add([grant]);
|
|
186
|
+
await caller.service.add([forged]);
|
|
187
|
+
expect(await caller.service.chainFor("http://provider", target)).toEqual([grant]);
|
|
188
|
+
expect(await caller.service.chainFor("http://other", target)).toEqual([forged]);
|
|
189
|
+
await expect(caller.service.delegate(grant.id, "key", [weather], { lifetimeMs: HOUR })).rejects.toThrow("name the issuer");
|
|
190
|
+
expect(await caller.service.delegate({ issuer: provider.publicKey, id: grant.id }, "key", [weather], { lifetimeMs: HOUR })).toHaveLength(2);
|
|
191
|
+
});
|
|
192
|
+
test("tries at most one renewal when every matching chain has expired", async () => {
|
|
193
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
194
|
+
const provider = await node("http://provider");
|
|
195
|
+
const caller = await node("http://caller");
|
|
196
|
+
for (let i = 0; i < 3; i++) {
|
|
197
|
+
const [grant] = await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR });
|
|
198
|
+
await caller.service.add([grant]);
|
|
199
|
+
await provider.service.revoke(grant.id);
|
|
200
|
+
}
|
|
201
|
+
vi.setSystemTime(Date.now() + 2 * HOUR);
|
|
202
|
+
expect(await caller.service.chainFor("http://provider", target)).toBeUndefined();
|
|
203
|
+
expect(fetch).toHaveBeenCalledOnce();
|
|
204
|
+
});
|
|
205
|
+
test("a revocation names the grant's issuer, so another issuer's grant with the same id is unaffected", async () => {
|
|
206
|
+
const provider = await node("http://provider");
|
|
207
|
+
const gateway = await node("http://gateway");
|
|
208
|
+
const user = await node("http://user");
|
|
209
|
+
const [root] = await provider.service.issue(gateway.publicKey, [weather], { lifetimeMs: HOUR });
|
|
210
|
+
await gateway.service.add([root]);
|
|
211
|
+
const chain = await gateway.service.delegate(root.id, user.publicKey, [weather], { lifetimeMs: HOUR });
|
|
212
|
+
// Revoking the root's id under the gateway's key, as when the gateway reuses that id for its own grant, must not hit the root.
|
|
213
|
+
await provider.service.revoke({ issuer: gateway.publicKey, id: root.id });
|
|
214
|
+
expect(await provider.service.verifyInvocation(user.service.authorize(chain, target), target)).toBe(true);
|
|
215
|
+
await provider.service.revoke({ issuer: gateway.publicKey, id: chain[1].id });
|
|
216
|
+
expect(await provider.service.verifyInvocation(user.service.authorize(chain, target), target)).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
test("copies expire by renewableUntil, and a past renewableUntil is refused", async () => {
|
|
219
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
220
|
+
const provider = await node("http://provider");
|
|
221
|
+
const caller = await node("http://caller");
|
|
222
|
+
const renewableUntil = new Date(Date.now() + 1.5 * HOUR).toISOString();
|
|
223
|
+
const [grant] = await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: 2 * HOUR, renewableUntil });
|
|
224
|
+
expect(grant.expiresAt).toBe(renewableUntil);
|
|
225
|
+
const [short] = await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR, renewableUntil });
|
|
226
|
+
await caller.service.add([short]);
|
|
227
|
+
vi.setSystemTime(Date.now() + 0.9 * HOUR);
|
|
228
|
+
expect((await caller.service.refreshDue()).renewed).toEqual([short.id]);
|
|
229
|
+
const [renewed] = (await caller.service.chainFor("http://provider", target));
|
|
230
|
+
expect(renewed.expiresAt).toBe(renewableUntil);
|
|
231
|
+
await expect(provider.service.issue("key", [weather], { lifetimeMs: HOUR, renewableUntil: new Date(Date.now() - 1).toISOString() })).rejects.toThrow("renewableUntil");
|
|
232
|
+
});
|
|
233
|
+
test("seed keeps a stored renewal and skips an expired chain", async () => {
|
|
234
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
235
|
+
const provider = await node("http://provider");
|
|
236
|
+
const caller = await node("http://caller");
|
|
237
|
+
const configured = await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR });
|
|
238
|
+
expect(await caller.service.seed(configured)).toBe(true);
|
|
239
|
+
vi.setSystemTime(Date.now() + 0.6 * HOUR);
|
|
240
|
+
await caller.service.refreshDue();
|
|
241
|
+
const renewed = (await caller.service.chainFor("http://provider", target));
|
|
242
|
+
expect(renewed[0].expiresAt).not.toBe(configured[0].expiresAt);
|
|
243
|
+
// A restart passes the configured chain again, after it has expired but while the renewal hasn't.
|
|
244
|
+
vi.setSystemTime(Date.now() + 0.5 * HOUR);
|
|
245
|
+
expect(await caller.service.seed(configured)).toBe(true);
|
|
246
|
+
expect(await caller.service.chainFor("http://provider", target)).toEqual(renewed);
|
|
247
|
+
const fresh = await node("http://fresh");
|
|
248
|
+
expect(await fresh.service.seed(configured)).toBe(false);
|
|
249
|
+
expect(await fresh.service.chainFor("http://provider", target)).toBeUndefined();
|
|
250
|
+
await expect(fresh.service.seed([{ ...configured[0], expiresAt: new Date(Date.now() + HOUR).toISOString() }])).rejects.toThrow("Invalid grant chain");
|
|
251
|
+
});
|
|
252
|
+
test("refuses a renewal that swaps in a different chain ending in the same id", async () => {
|
|
253
|
+
const provider = await node("http://provider");
|
|
254
|
+
const gateway = await node("http://gateway");
|
|
255
|
+
const middle = await node("http://middle");
|
|
256
|
+
const user = await node("http://user");
|
|
257
|
+
const [root] = await provider.service.issue(gateway.publicKey, [weather], { lifetimeMs: HOUR });
|
|
258
|
+
await gateway.service.add([root]);
|
|
259
|
+
const chain = await gateway.service.delegate(root.id, user.publicKey, [weather], { lifetimeMs: HOUR });
|
|
260
|
+
await user.service.add(chain);
|
|
261
|
+
const [, toMiddle] = await gateway.service.delegate(root.id, middle.publicKey, [weather], { lifetimeMs: HOUR });
|
|
262
|
+
const swapped = signGrant({ ...chain[1], issuer: middle.publicKey, issuerBaseUrl: "http://middle", parent: grantHash(toMiddle) }, middle.privateKey);
|
|
263
|
+
vi.mocked(fetch).mockResolvedValueOnce(new Response(JSON.stringify({ grants: [root, toMiddle, swapped] }), { status: 200 }));
|
|
264
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
265
|
+
vi.setSystemTime(Date.now() + 0.6 * HOUR);
|
|
266
|
+
const { failed } = await user.service.refreshDue();
|
|
267
|
+
expect(failed).toEqual([{ id: chain[1].id, error: expect.stringContaining("invalid chain") }]);
|
|
268
|
+
expect(await user.db.get("SELECT COUNT(*) AS count FROM held_grant")).toEqual({ count: 1 });
|
|
269
|
+
});
|
|
270
|
+
test("prefers a chain rooted at the provider's known key", async () => {
|
|
271
|
+
const provider = await node("http://provider");
|
|
272
|
+
const impostor = await node("http://impostor");
|
|
273
|
+
const caller = await node("http://caller");
|
|
274
|
+
const genuine = await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR });
|
|
275
|
+
// Rooted at the provider's URL, but signed by another key, and expiring later.
|
|
276
|
+
const stale = signGrant({ ...genuine[0], id: "stale", issuer: impostor.publicKey, expiresAt: new Date(Date.now() + 2 * HOUR).toISOString() }, impostor.privateKey);
|
|
277
|
+
await caller.service.add(genuine);
|
|
278
|
+
await caller.service.add([stale]);
|
|
279
|
+
expect(await caller.service.chainFor("http://provider", target)).toEqual([stale]);
|
|
280
|
+
await new PeerRepository(caller.db).create({ baseUrl: "http://provider", name: "provider", publicKey: provider.publicKey });
|
|
281
|
+
expect(await caller.service.chainFor("http://provider", target)).toEqual(genuine);
|
|
282
|
+
});
|
|
283
|
+
test("issue and delegate check their options", async () => {
|
|
284
|
+
const provider = await node("http://provider");
|
|
285
|
+
await expect(provider.service.issue("key", [weather], { lifetimeMs: 0 })).rejects.toThrow("lifetimeMs");
|
|
286
|
+
await expect(provider.service.issue("key", [weather], { lifetimeMs: 31 * 24 * HOUR })).rejects.toThrow("lifetimeMs");
|
|
287
|
+
await expect(provider.service.issue("key", [weather], { lifetimeMs: HOUR, renewableUntil: "soon" })).rejects.toThrow("renewableUntil");
|
|
288
|
+
await expect(provider.service.delegate("missing", "key", [weather], { lifetimeMs: HOUR })).rejects.toThrow("No held grant missing");
|
|
289
|
+
await expect(provider.service.issue("key", [], { lifetimeMs: HOUR })).rejects.toThrow("capabilities");
|
|
290
|
+
await expect(provider.service.issue("key", [{ resourcePath: "" }], { lifetimeMs: HOUR })).rejects.toThrow("capabilities");
|
|
291
|
+
});
|
|
292
|
+
describe("grant requests", () => {
|
|
293
|
+
const signedRequest = (keys, overrides = {}) => {
|
|
294
|
+
const unsigned = { publicKey: keys.publicKey, capabilities: [weather], details: null, nonce: crypto.randomUUID(), expiresAt: new Date(Date.now() + 60_000).toISOString(), ...overrides };
|
|
295
|
+
return { ...unsigned, signature: signData(grantRequestPayload(unsigned), keys.privateKey) };
|
|
296
|
+
};
|
|
297
|
+
test("an approved request yields a grant the provider accepts", async () => {
|
|
298
|
+
const approveGrant = vi.fn(({ details }) => details?.invite === "letmein" ? { lifetimeMs: HOUR } : undefined);
|
|
299
|
+
const provider = await node("http://provider", approveGrant);
|
|
300
|
+
const caller = await node("http://caller");
|
|
301
|
+
const chain = await caller.service.request("http://provider", [weather], { invite: "letmein" });
|
|
302
|
+
expect(approveGrant).toHaveBeenCalledExactlyOnceWith({ publicKey: caller.publicKey, capabilities: [weather], details: { invite: "letmein" } });
|
|
303
|
+
expect(chain).toEqual([expect.objectContaining({ audience: caller.publicKey, issuer: provider.publicKey, capabilities: [weather] })]);
|
|
304
|
+
const held = await caller.service.chainFor("http://provider", target);
|
|
305
|
+
expect(await provider.service.verifyInvocation(caller.service.authorize(held, target), target)).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
test("a refused request throws on the caller and stores nothing", async () => {
|
|
308
|
+
await node("http://provider", () => undefined);
|
|
309
|
+
const caller = await node("http://caller");
|
|
310
|
+
await expect(caller.service.request("http://provider", [weather])).rejects.toThrow("refused");
|
|
311
|
+
expect(await caller.service.chainFor("http://provider", target)).toBeUndefined();
|
|
312
|
+
});
|
|
313
|
+
test("the hook can grant narrower capabilities than requested", async () => {
|
|
314
|
+
const narrow = { ...weather, functionName: "humidity" };
|
|
315
|
+
const provider = await node("http://provider", () => ({ lifetimeMs: HOUR, capabilities: [narrow] }));
|
|
316
|
+
const caller = await node("http://caller");
|
|
317
|
+
const [grant] = await caller.service.request("http://provider", [weather]);
|
|
318
|
+
expect(grant.capabilities).toEqual([narrow]);
|
|
319
|
+
expect(await provider.service.verifyInvocation(caller.service.authorize([grant], target), target)).toBe(false);
|
|
320
|
+
});
|
|
321
|
+
test("without a hook every request is refused", async () => {
|
|
322
|
+
const provider = await node("http://provider");
|
|
323
|
+
const caller = await node("http://caller");
|
|
324
|
+
expect(provider.service.acceptsRequests).toBe(false);
|
|
325
|
+
expect(await provider.service.handleRequest(signedRequest(caller))).toBeUndefined();
|
|
326
|
+
});
|
|
327
|
+
test("the hook only sees signed, fresh, well-formed requests, each once", async () => {
|
|
328
|
+
const approveGrant = vi.fn(() => ({ lifetimeMs: HOUR }));
|
|
329
|
+
const provider = await node("http://provider", approveGrant);
|
|
330
|
+
const caller = await node("http://caller");
|
|
331
|
+
expect(await provider.service.handleRequest({ ...signedRequest(caller), signature: "forged" })).toBeUndefined();
|
|
332
|
+
expect(await provider.service.handleRequest({ ...signedRequest(caller), publicKey: generateBase64KeyPair().publicKey })).toBeUndefined();
|
|
333
|
+
expect(await provider.service.handleRequest(signedRequest(caller, { expiresAt: new Date(Date.now() - 1).toISOString() }))).toBeUndefined();
|
|
334
|
+
expect(await provider.service.handleRequest(signedRequest(caller, { expiresAt: new Date(Date.now() + HOUR).toISOString() }))).toBeUndefined();
|
|
335
|
+
expect(await provider.service.handleRequest(signedRequest(caller, { capabilities: [{ resourcePath: 3 }] }))).toBeUndefined();
|
|
336
|
+
expect(await provider.service.handleRequest(signedRequest(caller, { details: ["list"] }))).toBeUndefined();
|
|
337
|
+
expect(approveGrant).not.toHaveBeenCalled();
|
|
338
|
+
const request = signedRequest(caller);
|
|
339
|
+
expect(await provider.service.handleRequest(request)).toHaveLength(1);
|
|
340
|
+
expect(await provider.service.handleRequest(request)).toBeUndefined();
|
|
341
|
+
expect(approveGrant).toHaveBeenCalledOnce();
|
|
342
|
+
});
|
|
343
|
+
test("a request without details is signed and checked as null details", async () => {
|
|
344
|
+
const approveGrant = vi.fn(() => ({ lifetimeMs: HOUR }));
|
|
345
|
+
const provider = await node("http://provider", approveGrant);
|
|
346
|
+
const caller = await node("http://caller");
|
|
347
|
+
const { details: _, ...withoutDetails } = signedRequest(caller);
|
|
348
|
+
expect(await provider.service.handleRequest(withoutDetails)).toHaveLength(1);
|
|
349
|
+
expect(approveGrant).toHaveBeenCalledWith(expect.objectContaining({ details: null }));
|
|
350
|
+
});
|
|
351
|
+
test("a hook returning invalid options or capabilities is an error, not a grant", async () => {
|
|
352
|
+
const caller = await node("http://caller");
|
|
353
|
+
const badLifetime = await node("http://bad-lifetime", () => ({ lifetimeMs: -1 }));
|
|
354
|
+
const badCapabilities = await node("http://bad-capabilities", () => ({ lifetimeMs: HOUR, capabilities: [] }));
|
|
355
|
+
await expect(badLifetime.service.handleRequest(signedRequest(caller))).rejects.toThrow("lifetimeMs");
|
|
356
|
+
await expect(badCapabilities.service.handleRequest(signedRequest(caller))).rejects.toThrow("invalid approval");
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
describe("blocking callers", () => {
|
|
360
|
+
test("refuses a blocked caller's calls, renewals and requests until unblocked", async () => {
|
|
361
|
+
const provider = await node("http://provider", () => ({ lifetimeMs: HOUR }));
|
|
362
|
+
const caller = await node("http://caller");
|
|
363
|
+
await caller.service.add(await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR }));
|
|
364
|
+
const chain = (await caller.service.chainFor("http://provider", target));
|
|
365
|
+
await provider.service.block(caller.publicKey, "abuse");
|
|
366
|
+
expect(await provider.service.isBlocked(caller.publicKey)).toBe(true);
|
|
367
|
+
expect(await provider.service.verifyInvocation(caller.service.authorize(chain, target), target)).toBe(false);
|
|
368
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
369
|
+
vi.setSystemTime(Date.now() + 0.6 * HOUR);
|
|
370
|
+
expect((await caller.service.refreshDue()).failed).toHaveLength(1);
|
|
371
|
+
await expect(caller.service.request("http://provider", [weather])).rejects.toThrow("refused");
|
|
372
|
+
expect(await provider.service.unblock(caller.publicKey)).toBe(true);
|
|
373
|
+
expect(await provider.service.unblock(caller.publicKey)).toBe(false);
|
|
374
|
+
expect((await caller.service.refreshDue()).renewed).toHaveLength(1);
|
|
375
|
+
const renewed = (await caller.service.chainFor("http://provider", target));
|
|
376
|
+
expect(await provider.service.verifyInvocation(caller.service.authorize(renewed, target), target)).toBe(true);
|
|
377
|
+
});
|
|
378
|
+
test("blocking a delegator cuts off the grants it handed on", async () => {
|
|
379
|
+
const provider = await node("http://provider");
|
|
380
|
+
const gateway = await node("http://gateway");
|
|
381
|
+
const user = await node("http://user");
|
|
382
|
+
const [root] = await provider.service.issue(gateway.publicKey, [weather], { lifetimeMs: HOUR });
|
|
383
|
+
await gateway.service.add([root]);
|
|
384
|
+
const chain = await gateway.service.delegate(root.id, user.publicKey, [weather], { lifetimeMs: HOUR });
|
|
385
|
+
await provider.service.block(gateway.publicKey);
|
|
386
|
+
expect(await provider.service.verifyInvocation(user.service.authorize(chain, target), target)).toBe(false);
|
|
387
|
+
});
|
|
388
|
+
test("lists blocked callers in order and keeps the first block time", async () => {
|
|
389
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
390
|
+
const provider = await node("http://provider");
|
|
391
|
+
const [first, second] = [generateBase64KeyPair().publicKey, generateBase64KeyPair().publicKey];
|
|
392
|
+
await provider.service.block(first, "spam");
|
|
393
|
+
vi.setSystemTime(Date.now() + 1000);
|
|
394
|
+
await provider.service.block(second);
|
|
395
|
+
const [{ blockedAt }] = await provider.service.getBlocked(0, 1);
|
|
396
|
+
await provider.service.block(first, "still spam");
|
|
397
|
+
expect(await provider.service.getBlocked(0, 10)).toEqual([
|
|
398
|
+
{ publicKey: first, reason: "still spam", blockedAt },
|
|
399
|
+
{ publicKey: second, reason: null, blockedAt: expect.any(String) },
|
|
400
|
+
]);
|
|
401
|
+
expect(await provider.service.getBlocked(1, 10)).toHaveLength(1);
|
|
402
|
+
});
|
|
403
|
+
test("refuses to block something that is not a public key", async () => {
|
|
404
|
+
const provider = await node("http://provider");
|
|
405
|
+
await expect(provider.service.block("not-a-key")).rejects.toThrow("Invalid public key");
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
test("a renewal request must be signed by the grant's audience", async () => {
|
|
409
|
+
const provider = await node("http://provider");
|
|
410
|
+
const caller = await node("http://caller");
|
|
411
|
+
const [grant] = await provider.service.issue(caller.publicKey, [weather], { lifetimeMs: HOUR });
|
|
412
|
+
const request = { grantId: grant.id, publicKey: caller.publicKey, nonce: "n", expiresAt: new Date(Date.now() + 60_000).toISOString(), signature: "bad" };
|
|
413
|
+
expect(await provider.service.renew(request)).toBeUndefined();
|
|
414
|
+
});
|
|
415
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StellariumResource } from "../resource/repository.js";
|
|
2
|
+
import type { NodeConfig } from "../service.js";
|
|
3
|
+
export declare function generateBase64KeyPair(): {
|
|
4
|
+
publicKey: string;
|
|
5
|
+
privateKey: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const RECIPIENT = "4HkmXgXCJ8tw1ih5VFHetRt4kWdo1VLBVmV7B5p6ctK2";
|
|
8
|
+
export declare const COST = 10000;
|
|
9
|
+
export declare function testConfig(overrides?: Partial<NodeConfig>): NodeConfig;
|
|
10
|
+
/** The fixture provider's resource: paid `temperature` and free `humidity`. */
|
|
11
|
+
export declare function weatherResource(baseUrl?: string): StellariumResource;
|
|
12
|
+
export declare const temperatureSource = "request GetTemperature {\n const weather: com.example.Weather\n\n return weather.temperature(city: \"Oslo\")\n}";
|
|
13
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/tests/helpers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,wBAAgB,qBAAqB;IAM7B,SAAS;IACT,UAAU;EAEjB;AAED,eAAO,MAAM,SAAS,iDAAiD,CAAC;AACxE,eAAO,MAAM,IAAI,QAAQ,CAAC;AAE1B,wBAAgB,UAAU,CAAC,SAAS,GAAE,OAAO,CAAC,UAAU,CAAM,GAAG,UAAU,CAS1E;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,OAAO,SAA0B,GAAG,kBAAkB,CAuBrF;AAED,eAAO,MAAM,iBAAiB,sHAAoH,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
2
|
+
export function generateBase64KeyPair() {
|
|
3
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
|
|
4
|
+
publicKeyEncoding: { type: "spki", format: "der" },
|
|
5
|
+
privateKeyEncoding: { type: "pkcs8", format: "der" },
|
|
6
|
+
});
|
|
7
|
+
return {
|
|
8
|
+
publicKey: publicKey.toString("base64url"),
|
|
9
|
+
privateKey: privateKey.toString("base64url"),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export const RECIPIENT = "4HkmXgXCJ8tw1ih5VFHetRt4kWdo1VLBVmV7B5p6ctK2";
|
|
13
|
+
export const COST = 10000;
|
|
14
|
+
export function testConfig(overrides = {}) {
|
|
15
|
+
return {
|
|
16
|
+
baseUrl: "http://127.0.0.1:3002",
|
|
17
|
+
...generateBase64KeyPair(),
|
|
18
|
+
apiKey: "secret",
|
|
19
|
+
operatorUrl: "http://operator.invalid/v1",
|
|
20
|
+
costLimitLamports: 100_000,
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** The fixture provider's resource: paid `temperature` and free `humidity`. */
|
|
25
|
+
export function weatherResource(baseUrl = "http://127.0.0.1:3002") {
|
|
26
|
+
return {
|
|
27
|
+
id: 1,
|
|
28
|
+
baseUrl,
|
|
29
|
+
metadata: {},
|
|
30
|
+
path: "com.example",
|
|
31
|
+
name: "Weather",
|
|
32
|
+
functions: [
|
|
33
|
+
{
|
|
34
|
+
name: "temperature",
|
|
35
|
+
parameters: [{ name: "city", type: "string" }],
|
|
36
|
+
returnType: "int32",
|
|
37
|
+
exec: async (args) => (args.find((arg) => arg.name === "city")?.value === "Oslo" ? 12 : 20),
|
|
38
|
+
paymentManifest: { costLamports: COST, recipientAddress: RECIPIENT },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "humidity",
|
|
42
|
+
parameters: [{ name: "city", type: "string" }],
|
|
43
|
+
returnType: "int32",
|
|
44
|
+
exec: async () => 80,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export const temperatureSource = 'request GetTemperature {\n const weather: com.example.Weather\n\n return weather.temperature(city: "Oslo")\n}';
|