@mcp-i/core 0.1.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/LICENSE +21 -0
- package/README.md +390 -0
- package/dist/auth/handshake.d.ts +104 -0
- package/dist/auth/handshake.d.ts.map +1 -0
- package/dist/auth/handshake.js +230 -0
- package/dist/auth/handshake.js.map +1 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/types.d.ts +31 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +7 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/delegation/audience-validator.d.ts +9 -0
- package/dist/delegation/audience-validator.d.ts.map +1 -0
- package/dist/delegation/audience-validator.js +17 -0
- package/dist/delegation/audience-validator.js.map +1 -0
- package/dist/delegation/bitstring.d.ts +37 -0
- package/dist/delegation/bitstring.d.ts.map +1 -0
- package/dist/delegation/bitstring.js +117 -0
- package/dist/delegation/bitstring.js.map +1 -0
- package/dist/delegation/cascading-revocation.d.ts +45 -0
- package/dist/delegation/cascading-revocation.d.ts.map +1 -0
- package/dist/delegation/cascading-revocation.js +148 -0
- package/dist/delegation/cascading-revocation.js.map +1 -0
- package/dist/delegation/delegation-graph.d.ts +49 -0
- package/dist/delegation/delegation-graph.d.ts.map +1 -0
- package/dist/delegation/delegation-graph.js +99 -0
- package/dist/delegation/delegation-graph.js.map +1 -0
- package/dist/delegation/did-key-resolver.d.ts +64 -0
- package/dist/delegation/did-key-resolver.d.ts.map +1 -0
- package/dist/delegation/did-key-resolver.js +154 -0
- package/dist/delegation/did-key-resolver.js.map +1 -0
- package/dist/delegation/did-web-resolver.d.ts +83 -0
- package/dist/delegation/did-web-resolver.d.ts.map +1 -0
- package/dist/delegation/did-web-resolver.js +218 -0
- package/dist/delegation/did-web-resolver.js.map +1 -0
- package/dist/delegation/index.d.ts +21 -0
- package/dist/delegation/index.d.ts.map +1 -0
- package/dist/delegation/index.js +21 -0
- package/dist/delegation/index.js.map +1 -0
- package/dist/delegation/outbound-headers.d.ts +81 -0
- package/dist/delegation/outbound-headers.d.ts.map +1 -0
- package/dist/delegation/outbound-headers.js +139 -0
- package/dist/delegation/outbound-headers.js.map +1 -0
- package/dist/delegation/outbound-proof.d.ts +43 -0
- package/dist/delegation/outbound-proof.d.ts.map +1 -0
- package/dist/delegation/outbound-proof.js +52 -0
- package/dist/delegation/outbound-proof.js.map +1 -0
- package/dist/delegation/statuslist-manager.d.ts +44 -0
- package/dist/delegation/statuslist-manager.d.ts.map +1 -0
- package/dist/delegation/statuslist-manager.js +126 -0
- package/dist/delegation/statuslist-manager.js.map +1 -0
- package/dist/delegation/storage/memory-graph-storage.d.ts +70 -0
- package/dist/delegation/storage/memory-graph-storage.d.ts.map +1 -0
- package/dist/delegation/storage/memory-graph-storage.js +145 -0
- package/dist/delegation/storage/memory-graph-storage.js.map +1 -0
- package/dist/delegation/storage/memory-statuslist-storage.d.ts +19 -0
- package/dist/delegation/storage/memory-statuslist-storage.d.ts.map +1 -0
- package/dist/delegation/storage/memory-statuslist-storage.js +33 -0
- package/dist/delegation/storage/memory-statuslist-storage.js.map +1 -0
- package/dist/delegation/utils.d.ts +49 -0
- package/dist/delegation/utils.d.ts.map +1 -0
- package/dist/delegation/utils.js +131 -0
- package/dist/delegation/utils.js.map +1 -0
- package/dist/delegation/vc-issuer.d.ts +56 -0
- package/dist/delegation/vc-issuer.d.ts.map +1 -0
- package/dist/delegation/vc-issuer.js +80 -0
- package/dist/delegation/vc-issuer.js.map +1 -0
- package/dist/delegation/vc-verifier.d.ts +112 -0
- package/dist/delegation/vc-verifier.d.ts.map +1 -0
- package/dist/delegation/vc-verifier.js +280 -0
- package/dist/delegation/vc-verifier.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/index.d.ts +2 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/logging/index.js +2 -0
- package/dist/logging/index.js.map +1 -0
- package/dist/logging/logger.d.ts +23 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +82 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/middleware/index.d.ts +7 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +7 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/with-mcpi.d.ts +152 -0
- package/dist/middleware/with-mcpi.d.ts.map +1 -0
- package/dist/middleware/with-mcpi.js +472 -0
- package/dist/middleware/with-mcpi.js.map +1 -0
- package/dist/proof/errors.d.ts +49 -0
- package/dist/proof/errors.d.ts.map +1 -0
- package/dist/proof/errors.js +61 -0
- package/dist/proof/errors.js.map +1 -0
- package/dist/proof/generator.d.ts +65 -0
- package/dist/proof/generator.d.ts.map +1 -0
- package/dist/proof/generator.js +163 -0
- package/dist/proof/generator.js.map +1 -0
- package/dist/proof/index.d.ts +4 -0
- package/dist/proof/index.d.ts.map +1 -0
- package/dist/proof/index.js +4 -0
- package/dist/proof/index.js.map +1 -0
- package/dist/proof/verifier.d.ts +108 -0
- package/dist/proof/verifier.d.ts.map +1 -0
- package/dist/proof/verifier.js +299 -0
- package/dist/proof/verifier.js.map +1 -0
- package/dist/providers/base.d.ts +64 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +19 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/index.d.ts +3 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +3 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/memory.d.ts +33 -0
- package/dist/providers/memory.d.ts.map +1 -0
- package/dist/providers/memory.js +102 -0
- package/dist/providers/memory.js.map +1 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +2 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/manager.d.ts +77 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +251 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/types/protocol.d.ts +320 -0
- package/dist/types/protocol.d.ts.map +1 -0
- package/dist/types/protocol.js +229 -0
- package/dist/types/protocol.js.map +1 -0
- package/dist/utils/base58.d.ts +31 -0
- package/dist/utils/base58.d.ts.map +1 -0
- package/dist/utils/base58.js +104 -0
- package/dist/utils/base58.js.map +1 -0
- package/dist/utils/base64.d.ts +13 -0
- package/dist/utils/base64.d.ts.map +1 -0
- package/dist/utils/base64.js +99 -0
- package/dist/utils/base64.js.map +1 -0
- package/dist/utils/crypto-service.d.ts +37 -0
- package/dist/utils/crypto-service.d.ts.map +1 -0
- package/dist/utils/crypto-service.js +153 -0
- package/dist/utils/crypto-service.js.map +1 -0
- package/dist/utils/did-helpers.d.ts +156 -0
- package/dist/utils/did-helpers.d.ts.map +1 -0
- package/dist/utils/did-helpers.js +193 -0
- package/dist/utils/did-helpers.js.map +1 -0
- package/dist/utils/ed25519-constants.d.ts +18 -0
- package/dist/utils/ed25519-constants.d.ts.map +1 -0
- package/dist/utils/ed25519-constants.js +21 -0
- package/dist/utils/ed25519-constants.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +105 -0
- package/src/__tests__/integration/full-flow.test.ts +362 -0
- package/src/__tests__/providers/base.test.ts +173 -0
- package/src/__tests__/providers/memory.test.ts +332 -0
- package/src/__tests__/utils/mock-providers.ts +319 -0
- package/src/__tests__/utils/node-crypto-provider.ts +93 -0
- package/src/auth/handshake.ts +411 -0
- package/src/auth/index.ts +11 -0
- package/src/auth/types.ts +40 -0
- package/src/delegation/__tests__/audience-validator.test.ts +110 -0
- package/src/delegation/__tests__/bitstring.test.ts +346 -0
- package/src/delegation/__tests__/cascading-revocation.test.ts +624 -0
- package/src/delegation/__tests__/delegation-graph.test.ts +623 -0
- package/src/delegation/__tests__/did-key-resolver.test.ts +265 -0
- package/src/delegation/__tests__/did-web-resolver.test.ts +467 -0
- package/src/delegation/__tests__/outbound-headers.test.ts +230 -0
- package/src/delegation/__tests__/outbound-proof.test.ts +179 -0
- package/src/delegation/__tests__/statuslist-manager.test.ts +515 -0
- package/src/delegation/__tests__/utils.test.ts +185 -0
- package/src/delegation/__tests__/vc-issuer.test.ts +487 -0
- package/src/delegation/__tests__/vc-verifier.test.ts +1029 -0
- package/src/delegation/audience-validator.ts +24 -0
- package/src/delegation/bitstring.ts +160 -0
- package/src/delegation/cascading-revocation.ts +224 -0
- package/src/delegation/delegation-graph.ts +143 -0
- package/src/delegation/did-key-resolver.ts +181 -0
- package/src/delegation/did-web-resolver.ts +270 -0
- package/src/delegation/index.ts +33 -0
- package/src/delegation/outbound-headers.ts +193 -0
- package/src/delegation/outbound-proof.ts +90 -0
- package/src/delegation/statuslist-manager.ts +219 -0
- package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
- package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
- package/src/delegation/storage/memory-graph-storage.ts +178 -0
- package/src/delegation/storage/memory-statuslist-storage.ts +42 -0
- package/src/delegation/utils.ts +189 -0
- package/src/delegation/vc-issuer.ts +137 -0
- package/src/delegation/vc-verifier.ts +440 -0
- package/src/index.ts +264 -0
- package/src/logging/__tests__/logger.test.ts +366 -0
- package/src/logging/index.ts +6 -0
- package/src/logging/logger.ts +91 -0
- package/src/middleware/__tests__/with-mcpi.test.ts +504 -0
- package/src/middleware/index.ts +16 -0
- package/src/middleware/with-mcpi.ts +766 -0
- package/src/proof/__tests__/proof-generator.test.ts +483 -0
- package/src/proof/__tests__/verifier.test.ts +488 -0
- package/src/proof/errors.ts +75 -0
- package/src/proof/generator.ts +255 -0
- package/src/proof/index.ts +22 -0
- package/src/proof/verifier.ts +449 -0
- package/src/providers/base.ts +68 -0
- package/src/providers/index.ts +15 -0
- package/src/providers/memory.ts +130 -0
- package/src/session/__tests__/session-manager.test.ts +342 -0
- package/src/session/index.ts +7 -0
- package/src/session/manager.ts +332 -0
- package/src/types/protocol.ts +596 -0
- package/src/utils/__tests__/base58.test.ts +281 -0
- package/src/utils/__tests__/base64.test.ts +239 -0
- package/src/utils/__tests__/crypto-service.test.ts +530 -0
- package/src/utils/__tests__/did-helpers.test.ts +156 -0
- package/src/utils/base58.ts +115 -0
- package/src/utils/base64.ts +116 -0
- package/src/utils/crypto-service.ts +209 -0
- package/src/utils/did-helpers.ts +210 -0
- package/src/utils/ed25519-constants.ts +23 -0
- package/src/utils/index.ts +9 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound Delegation Proof Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for buildDelegationProofJWT and buildChainString helpers.
|
|
5
|
+
* All 8 test cases from spec Section 5.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
import { generateKeyPair, exportJWK, decodeJwt, decodeProtectedHeader } from "jose";
|
|
10
|
+
import {
|
|
11
|
+
buildDelegationProofJWT,
|
|
12
|
+
buildChainString,
|
|
13
|
+
type DelegationProofOptions,
|
|
14
|
+
type Ed25519PrivateJWK,
|
|
15
|
+
} from "../outbound-proof.js";
|
|
16
|
+
import type { DelegationRecord } from "../../types/protocol.js";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Test key helpers
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
async function generateTestKeyPair(): Promise<{
|
|
23
|
+
privateKeyJwk: Ed25519PrivateJWK;
|
|
24
|
+
kid: string;
|
|
25
|
+
}> {
|
|
26
|
+
const { privateKey } = await generateKeyPair("EdDSA", { crv: "Ed25519" });
|
|
27
|
+
const jwk = await exportJWK(privateKey);
|
|
28
|
+
const privateKeyJwk: Ed25519PrivateJWK = {
|
|
29
|
+
kty: "OKP",
|
|
30
|
+
crv: "Ed25519",
|
|
31
|
+
x: jwk.x as string,
|
|
32
|
+
d: jwk.d as string,
|
|
33
|
+
};
|
|
34
|
+
return { privateKeyJwk, kid: "did:web:agent.example.com#key-1" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const AGENT_DID = "did:web:agent.example.com";
|
|
38
|
+
const USER_DID = "did:web:alice.example.com";
|
|
39
|
+
const DELEGATION_ID = "del-abc-123";
|
|
40
|
+
const DELEGATION_CHAIN = "vc-abc>del-abc-123";
|
|
41
|
+
const SCOPES = ["read_files", "write_calendar"];
|
|
42
|
+
const TARGET_HOSTNAME = "api.example.com";
|
|
43
|
+
|
|
44
|
+
function baseOptions(overrides: Partial<DelegationProofOptions> = {}): DelegationProofOptions {
|
|
45
|
+
return {
|
|
46
|
+
agentDid: AGENT_DID,
|
|
47
|
+
userDid: USER_DID,
|
|
48
|
+
delegationId: DELEGATION_ID,
|
|
49
|
+
delegationChain: DELEGATION_CHAIN,
|
|
50
|
+
scopes: SCOPES,
|
|
51
|
+
privateKeyJwk: {} as Ed25519PrivateJWK, // overridden in test setup
|
|
52
|
+
kid: "did:web:agent.example.com#key-1",
|
|
53
|
+
targetHostname: TARGET_HOSTNAME,
|
|
54
|
+
...overrides,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// buildDelegationProofJWT tests
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
describe("buildDelegationProofJWT", () => {
|
|
63
|
+
it("happy path — produces a valid EdDSA compact JWT", async () => {
|
|
64
|
+
const { privateKeyJwk, kid } = await generateTestKeyPair();
|
|
65
|
+
const jwt = await buildDelegationProofJWT(baseOptions({ privateKeyJwk, kid }));
|
|
66
|
+
|
|
67
|
+
expect(typeof jwt).toBe("string");
|
|
68
|
+
// Compact JWT has exactly 3 parts
|
|
69
|
+
expect(jwt.split(".")).toHaveLength(3);
|
|
70
|
+
|
|
71
|
+
const header = decodeProtectedHeader(jwt);
|
|
72
|
+
expect(header.alg).toBe("EdDSA");
|
|
73
|
+
expect(header.kid).toBe(kid);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("JWT payload contains all required claims", async () => {
|
|
77
|
+
const { privateKeyJwk, kid } = await generateTestKeyPair();
|
|
78
|
+
const jwt = await buildDelegationProofJWT(baseOptions({ privateKeyJwk, kid }));
|
|
79
|
+
|
|
80
|
+
const payload = decodeJwt(jwt);
|
|
81
|
+
expect(payload.iss).toBe(AGENT_DID);
|
|
82
|
+
expect(payload.sub).toBe(USER_DID);
|
|
83
|
+
expect(payload.aud).toBe(TARGET_HOSTNAME);
|
|
84
|
+
expect(typeof payload.jti).toBe("string");
|
|
85
|
+
expect(payload.delegation_id).toBe(DELEGATION_ID);
|
|
86
|
+
expect(payload.delegation_chain).toBe(DELEGATION_CHAIN);
|
|
87
|
+
expect(payload.scope).toBe(SCOPES.join(","));
|
|
88
|
+
expect(typeof payload.iat).toBe("number");
|
|
89
|
+
expect(typeof payload.exp).toBe("number");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("exp is exactly 60 seconds from iat", async () => {
|
|
93
|
+
const { privateKeyJwk, kid } = await generateTestKeyPair();
|
|
94
|
+
const jwt = await buildDelegationProofJWT(baseOptions({ privateKeyJwk, kid }));
|
|
95
|
+
|
|
96
|
+
const payload = decodeJwt(jwt);
|
|
97
|
+
expect((payload.exp as number) - (payload.iat as number)).toBe(60);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("jti is unique across multiple calls", async () => {
|
|
101
|
+
const { privateKeyJwk, kid } = await generateTestKeyPair();
|
|
102
|
+
const opts = baseOptions({ privateKeyJwk, kid });
|
|
103
|
+
|
|
104
|
+
const jwt1 = await buildDelegationProofJWT(opts);
|
|
105
|
+
const jwt2 = await buildDelegationProofJWT(opts);
|
|
106
|
+
|
|
107
|
+
const payload1 = decodeJwt(jwt1);
|
|
108
|
+
const payload2 = decodeJwt(jwt2);
|
|
109
|
+
expect(payload1.jti).not.toBe(payload2.jti);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("throws when private key is invalid", async () => {
|
|
113
|
+
const badKeyJwk: Ed25519PrivateJWK = {
|
|
114
|
+
kty: "OKP",
|
|
115
|
+
crv: "Ed25519",
|
|
116
|
+
x: "invalid",
|
|
117
|
+
d: "invalid",
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
await expect(
|
|
121
|
+
buildDelegationProofJWT(baseOptions({ privateKeyJwk: badKeyJwk }))
|
|
122
|
+
).rejects.toThrow();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// buildChainString tests
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
describe("buildChainString", () => {
|
|
131
|
+
const baseDelegation: DelegationRecord = {
|
|
132
|
+
id: "del-123",
|
|
133
|
+
issuerDid: "did:web:issuer.example.com",
|
|
134
|
+
subjectDid: "did:web:agent.example.com",
|
|
135
|
+
vcId: "vc-abc",
|
|
136
|
+
constraints: { scopes: ["read:files"] },
|
|
137
|
+
signature: "sig",
|
|
138
|
+
status: "active",
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
it("single-hop delegation encodes correctly", () => {
|
|
142
|
+
const result = buildChainString(baseDelegation);
|
|
143
|
+
expect(result).toBe("vc-abc>del-123");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("multi-hop (3 levels) encodes correctly when concatenated", () => {
|
|
147
|
+
const del1: DelegationRecord = { ...baseDelegation, id: "del-1", vcId: "vc-1" };
|
|
148
|
+
const del2: DelegationRecord = { ...baseDelegation, id: "del-2", vcId: "vc-2", parentId: "del-1" };
|
|
149
|
+
const del3: DelegationRecord = { ...baseDelegation, id: "del-3", vcId: "vc-3", parentId: "del-2" };
|
|
150
|
+
|
|
151
|
+
// Build the chain by concatenating each hop (caller responsibility for multi-hop)
|
|
152
|
+
const chain = [del1, del2, del3].map(buildChainString).join(">");
|
|
153
|
+
expect(chain).toBe("vc-1>del-1>vc-2>del-2>vc-3>del-3");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("returns delegation.id when vcId is absent", () => {
|
|
157
|
+
const delegation: DelegationRecord = {
|
|
158
|
+
...baseDelegation,
|
|
159
|
+
vcId: undefined as unknown as string,
|
|
160
|
+
};
|
|
161
|
+
const result = buildChainString(delegation);
|
|
162
|
+
expect(result).toBe("del-123");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("empty chain returns empty string gracefully", () => {
|
|
166
|
+
const emptyDelegation = {
|
|
167
|
+
id: "",
|
|
168
|
+
issuerDid: "did:web:issuer.example.com",
|
|
169
|
+
subjectDid: "did:web:agent.example.com",
|
|
170
|
+
vcId: undefined as unknown as string,
|
|
171
|
+
constraints: { scopes: [] },
|
|
172
|
+
signature: "sig",
|
|
173
|
+
status: "active" as const,
|
|
174
|
+
} satisfies DelegationRecord;
|
|
175
|
+
|
|
176
|
+
const result = buildChainString(emptyDelegation);
|
|
177
|
+
expect(result).toBe("");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for StatusList2021Manager
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Status entry allocation
|
|
6
|
+
* - Status updates (revoke/restore)
|
|
7
|
+
* - Status checking
|
|
8
|
+
* - Status list creation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
StatusList2021Manager,
|
|
14
|
+
createStatusListManager,
|
|
15
|
+
type StatusListStorageProvider,
|
|
16
|
+
type StatusListIdentityProvider,
|
|
17
|
+
} from "../statuslist-manager.js";
|
|
18
|
+
import type { VCSigningFunction } from "../vc-issuer.js";
|
|
19
|
+
import type { CompressionFunction, DecompressionFunction } from "../bitstring.js";
|
|
20
|
+
import type {
|
|
21
|
+
StatusList2021Credential,
|
|
22
|
+
CredentialStatus,
|
|
23
|
+
} from "../../types/protocol.js";
|
|
24
|
+
|
|
25
|
+
describe("StatusList2021Manager", () => {
|
|
26
|
+
let mockStorage: StatusListStorageProvider;
|
|
27
|
+
let mockIdentity: StatusListIdentityProvider;
|
|
28
|
+
let mockSigningFunction: VCSigningFunction;
|
|
29
|
+
let mockCompressor: CompressionFunction;
|
|
30
|
+
let mockDecompressor: DecompressionFunction;
|
|
31
|
+
|
|
32
|
+
// Create a simple bitstring representation for testing
|
|
33
|
+
const createEncodedList = (size: number = 16): string => {
|
|
34
|
+
// Create a simple base64url encoded "bitstring" for testing
|
|
35
|
+
// This represents an empty list (all zeros)
|
|
36
|
+
const bytes = new Uint8Array(Math.ceil(size / 8));
|
|
37
|
+
return Buffer.from(bytes).toString("base64url");
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const createStatusListCredential = (
|
|
41
|
+
id: string,
|
|
42
|
+
purpose: "revocation" | "suspension",
|
|
43
|
+
encodedList?: string
|
|
44
|
+
): StatusList2021Credential => ({
|
|
45
|
+
"@context": [
|
|
46
|
+
"https://www.w3.org/2018/credentials/v1",
|
|
47
|
+
"https://w3id.org/vc/status-list/2021/v1",
|
|
48
|
+
],
|
|
49
|
+
id,
|
|
50
|
+
type: ["VerifiableCredential", "StatusList2021Credential"],
|
|
51
|
+
issuer: "did:key:z6MkIssuer",
|
|
52
|
+
issuanceDate: new Date().toISOString(),
|
|
53
|
+
credentialSubject: {
|
|
54
|
+
id: `${id}#list`,
|
|
55
|
+
type: "StatusList2021",
|
|
56
|
+
statusPurpose: purpose,
|
|
57
|
+
encodedList: encodedList || createEncodedList(),
|
|
58
|
+
},
|
|
59
|
+
proof: {
|
|
60
|
+
type: "Ed25519Signature2020",
|
|
61
|
+
created: new Date().toISOString(),
|
|
62
|
+
verificationMethod: "did:key:z6MkIssuer#keys-1",
|
|
63
|
+
proofPurpose: "assertionMethod",
|
|
64
|
+
proofValue: "mock-proof-value",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
mockStorage = {
|
|
70
|
+
getStatusList: vi.fn().mockResolvedValue(null),
|
|
71
|
+
setStatusList: vi.fn().mockResolvedValue(undefined),
|
|
72
|
+
allocateIndex: vi.fn().mockResolvedValue(0),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
mockIdentity = {
|
|
76
|
+
getDid: vi.fn().mockReturnValue("did:key:z6MkTestIssuer"),
|
|
77
|
+
getKeyId: vi.fn().mockReturnValue("did:key:z6MkTestIssuer#keys-1"),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
mockSigningFunction = vi.fn().mockResolvedValue({
|
|
81
|
+
type: "Ed25519Signature2020",
|
|
82
|
+
created: new Date().toISOString(),
|
|
83
|
+
verificationMethod: "did:key:z6MkTestIssuer#keys-1",
|
|
84
|
+
proofPurpose: "assertionMethod",
|
|
85
|
+
proofValue: "mock-signature",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Simple mock compressor/decompressor (just passes through for testing)
|
|
89
|
+
mockCompressor = {
|
|
90
|
+
compress: vi.fn().mockImplementation(async (data: Uint8Array) => data),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
mockDecompressor = {
|
|
94
|
+
decompress: vi.fn().mockImplementation(async (data: Uint8Array) => data),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("constructor and configuration", () => {
|
|
99
|
+
it("should use default configuration values", () => {
|
|
100
|
+
const manager = new StatusList2021Manager(
|
|
101
|
+
mockStorage,
|
|
102
|
+
mockIdentity,
|
|
103
|
+
mockSigningFunction,
|
|
104
|
+
mockCompressor,
|
|
105
|
+
mockDecompressor
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(manager.getStatusListBaseUrl()).toBe("https://status.example.com");
|
|
109
|
+
expect(manager.getDefaultListSize()).toBe(131072); // 128K
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should accept custom configuration values", () => {
|
|
113
|
+
const manager = new StatusList2021Manager(
|
|
114
|
+
mockStorage,
|
|
115
|
+
mockIdentity,
|
|
116
|
+
mockSigningFunction,
|
|
117
|
+
mockCompressor,
|
|
118
|
+
mockDecompressor,
|
|
119
|
+
{
|
|
120
|
+
statusListBaseUrl: "https://custom.example.com/status",
|
|
121
|
+
defaultListSize: 65536,
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect(manager.getStatusListBaseUrl()).toBe(
|
|
126
|
+
"https://custom.example.com/status"
|
|
127
|
+
);
|
|
128
|
+
expect(manager.getDefaultListSize()).toBe(65536);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("allocateStatusEntry", () => {
|
|
133
|
+
it("should allocate a revocation status entry", async () => {
|
|
134
|
+
const manager = new StatusList2021Manager(
|
|
135
|
+
mockStorage,
|
|
136
|
+
mockIdentity,
|
|
137
|
+
mockSigningFunction,
|
|
138
|
+
mockCompressor,
|
|
139
|
+
mockDecompressor
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
vi.mocked(mockStorage.allocateIndex).mockResolvedValue(42);
|
|
143
|
+
|
|
144
|
+
const entry = await manager.allocateStatusEntry("revocation");
|
|
145
|
+
|
|
146
|
+
expect(entry).toEqual({
|
|
147
|
+
id: "https://status.example.com/revocation/v1#42",
|
|
148
|
+
type: "StatusList2021Entry",
|
|
149
|
+
statusPurpose: "revocation",
|
|
150
|
+
statusListIndex: "42",
|
|
151
|
+
statusListCredential: "https://status.example.com/revocation/v1",
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(mockStorage.allocateIndex).toHaveBeenCalledWith(
|
|
155
|
+
"https://status.example.com/revocation/v1"
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should allocate a suspension status entry", async () => {
|
|
160
|
+
const manager = new StatusList2021Manager(
|
|
161
|
+
mockStorage,
|
|
162
|
+
mockIdentity,
|
|
163
|
+
mockSigningFunction,
|
|
164
|
+
mockCompressor,
|
|
165
|
+
mockDecompressor
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
vi.mocked(mockStorage.allocateIndex).mockResolvedValue(100);
|
|
169
|
+
|
|
170
|
+
const entry = await manager.allocateStatusEntry("suspension");
|
|
171
|
+
|
|
172
|
+
expect(entry).toEqual({
|
|
173
|
+
id: "https://status.example.com/suspension/v1#100",
|
|
174
|
+
type: "StatusList2021Entry",
|
|
175
|
+
statusPurpose: "suspension",
|
|
176
|
+
statusListIndex: "100",
|
|
177
|
+
statusListCredential: "https://status.example.com/suspension/v1",
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should create status list if it doesn't exist", async () => {
|
|
182
|
+
const manager = new StatusList2021Manager(
|
|
183
|
+
mockStorage,
|
|
184
|
+
mockIdentity,
|
|
185
|
+
mockSigningFunction,
|
|
186
|
+
mockCompressor,
|
|
187
|
+
mockDecompressor
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
vi.mocked(mockStorage.getStatusList).mockResolvedValue(null);
|
|
191
|
+
|
|
192
|
+
await manager.allocateStatusEntry("revocation");
|
|
193
|
+
|
|
194
|
+
// Should check if status list exists
|
|
195
|
+
expect(mockStorage.getStatusList).toHaveBeenCalled();
|
|
196
|
+
// Should create new status list
|
|
197
|
+
expect(mockStorage.setStatusList).toHaveBeenCalledWith(
|
|
198
|
+
"https://status.example.com/revocation/v1",
|
|
199
|
+
expect.objectContaining({
|
|
200
|
+
type: ["VerifiableCredential", "StatusList2021Credential"],
|
|
201
|
+
credentialSubject: expect.objectContaining({
|
|
202
|
+
type: "StatusList2021",
|
|
203
|
+
statusPurpose: "revocation",
|
|
204
|
+
}),
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should not create status list if it already exists", async () => {
|
|
210
|
+
const manager = new StatusList2021Manager(
|
|
211
|
+
mockStorage,
|
|
212
|
+
mockIdentity,
|
|
213
|
+
mockSigningFunction,
|
|
214
|
+
mockCompressor,
|
|
215
|
+
mockDecompressor
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const existingCredential = createStatusListCredential(
|
|
219
|
+
"https://status.example.com/revocation/v1",
|
|
220
|
+
"revocation"
|
|
221
|
+
);
|
|
222
|
+
vi.mocked(mockStorage.getStatusList).mockResolvedValue(
|
|
223
|
+
existingCredential
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
await manager.allocateStatusEntry("revocation");
|
|
227
|
+
|
|
228
|
+
// Should check if exists
|
|
229
|
+
expect(mockStorage.getStatusList).toHaveBeenCalled();
|
|
230
|
+
// Should NOT create new one
|
|
231
|
+
expect(mockStorage.setStatusList).not.toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("updateStatus", () => {
|
|
236
|
+
it("should revoke a credential by setting its status bit", async () => {
|
|
237
|
+
const manager = new StatusList2021Manager(
|
|
238
|
+
mockStorage,
|
|
239
|
+
mockIdentity,
|
|
240
|
+
mockSigningFunction,
|
|
241
|
+
mockCompressor,
|
|
242
|
+
mockDecompressor
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const statusListId = "https://status.example.com/revocation/v1";
|
|
246
|
+
const existingCredential = createStatusListCredential(
|
|
247
|
+
statusListId,
|
|
248
|
+
"revocation"
|
|
249
|
+
);
|
|
250
|
+
vi.mocked(mockStorage.getStatusList).mockResolvedValue(
|
|
251
|
+
existingCredential
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const credentialStatus: CredentialStatus = {
|
|
255
|
+
id: `${statusListId}#5`,
|
|
256
|
+
type: "StatusList2021Entry",
|
|
257
|
+
statusPurpose: "revocation",
|
|
258
|
+
statusListIndex: "5",
|
|
259
|
+
statusListCredential: statusListId,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
await manager.updateStatus(credentialStatus, true);
|
|
263
|
+
|
|
264
|
+
// Should save updated status list
|
|
265
|
+
expect(mockStorage.setStatusList).toHaveBeenCalledWith(
|
|
266
|
+
statusListId,
|
|
267
|
+
expect.objectContaining({
|
|
268
|
+
credentialSubject: expect.objectContaining({
|
|
269
|
+
encodedList: expect.any(String),
|
|
270
|
+
}),
|
|
271
|
+
proof: expect.any(Object),
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("should restore a credential by clearing its status bit", async () => {
|
|
277
|
+
const manager = new StatusList2021Manager(
|
|
278
|
+
mockStorage,
|
|
279
|
+
mockIdentity,
|
|
280
|
+
mockSigningFunction,
|
|
281
|
+
mockCompressor,
|
|
282
|
+
mockDecompressor
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const statusListId = "https://status.example.com/revocation/v1";
|
|
286
|
+
// Create a credential where bit 5 is already set
|
|
287
|
+
const bytes = new Uint8Array(2);
|
|
288
|
+
bytes[0] = 0b00100000; // Bit 5 is set
|
|
289
|
+
const encodedList = Buffer.from(bytes).toString("base64url");
|
|
290
|
+
const existingCredential = createStatusListCredential(
|
|
291
|
+
statusListId,
|
|
292
|
+
"revocation",
|
|
293
|
+
encodedList
|
|
294
|
+
);
|
|
295
|
+
vi.mocked(mockStorage.getStatusList).mockResolvedValue(
|
|
296
|
+
existingCredential
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const credentialStatus: CredentialStatus = {
|
|
300
|
+
id: `${statusListId}#5`,
|
|
301
|
+
type: "StatusList2021Entry",
|
|
302
|
+
statusPurpose: "revocation",
|
|
303
|
+
statusListIndex: "5",
|
|
304
|
+
statusListCredential: statusListId,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
await manager.updateStatus(credentialStatus, false);
|
|
308
|
+
|
|
309
|
+
// Should re-sign and save
|
|
310
|
+
expect(mockSigningFunction).toHaveBeenCalled();
|
|
311
|
+
expect(mockStorage.setStatusList).toHaveBeenCalled();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("should throw error if status list not found", async () => {
|
|
315
|
+
const manager = new StatusList2021Manager(
|
|
316
|
+
mockStorage,
|
|
317
|
+
mockIdentity,
|
|
318
|
+
mockSigningFunction,
|
|
319
|
+
mockCompressor,
|
|
320
|
+
mockDecompressor
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
vi.mocked(mockStorage.getStatusList).mockResolvedValue(null);
|
|
324
|
+
|
|
325
|
+
const credentialStatus: CredentialStatus = {
|
|
326
|
+
id: "https://status.example.com/revocation/v1#5",
|
|
327
|
+
type: "StatusList2021Entry",
|
|
328
|
+
statusPurpose: "revocation",
|
|
329
|
+
statusListIndex: "5",
|
|
330
|
+
statusListCredential: "https://status.example.com/revocation/v1",
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
await expect(
|
|
334
|
+
manager.updateStatus(credentialStatus, true)
|
|
335
|
+
).rejects.toThrow("Status list not found");
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("checkStatus", () => {
|
|
340
|
+
it("should return false for non-revoked credential", async () => {
|
|
341
|
+
const manager = new StatusList2021Manager(
|
|
342
|
+
mockStorage,
|
|
343
|
+
mockIdentity,
|
|
344
|
+
mockSigningFunction,
|
|
345
|
+
mockCompressor,
|
|
346
|
+
mockDecompressor
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const statusListId = "https://status.example.com/revocation/v1";
|
|
350
|
+
// Empty bitstring (all zeros)
|
|
351
|
+
const existingCredential = createStatusListCredential(
|
|
352
|
+
statusListId,
|
|
353
|
+
"revocation"
|
|
354
|
+
);
|
|
355
|
+
vi.mocked(mockStorage.getStatusList).mockResolvedValue(
|
|
356
|
+
existingCredential
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const credentialStatus: CredentialStatus = {
|
|
360
|
+
id: `${statusListId}#5`,
|
|
361
|
+
type: "StatusList2021Entry",
|
|
362
|
+
statusPurpose: "revocation",
|
|
363
|
+
statusListIndex: "5",
|
|
364
|
+
statusListCredential: statusListId,
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const isRevoked = await manager.checkStatus(credentialStatus);
|
|
368
|
+
|
|
369
|
+
expect(isRevoked).toBe(false);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("should return true for revoked credential", async () => {
|
|
373
|
+
const manager = new StatusList2021Manager(
|
|
374
|
+
mockStorage,
|
|
375
|
+
mockIdentity,
|
|
376
|
+
mockSigningFunction,
|
|
377
|
+
mockCompressor,
|
|
378
|
+
mockDecompressor
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const statusListId = "https://status.example.com/revocation/v1";
|
|
382
|
+
// Create bitstring with bit 5 set
|
|
383
|
+
const bytes = new Uint8Array(2);
|
|
384
|
+
bytes[0] = 0b00100000; // Bit 5 is set
|
|
385
|
+
const encodedList = Buffer.from(bytes).toString("base64url");
|
|
386
|
+
const existingCredential = createStatusListCredential(
|
|
387
|
+
statusListId,
|
|
388
|
+
"revocation",
|
|
389
|
+
encodedList
|
|
390
|
+
);
|
|
391
|
+
vi.mocked(mockStorage.getStatusList).mockResolvedValue(
|
|
392
|
+
existingCredential
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const credentialStatus: CredentialStatus = {
|
|
396
|
+
id: `${statusListId}#5`,
|
|
397
|
+
type: "StatusList2021Entry",
|
|
398
|
+
statusPurpose: "revocation",
|
|
399
|
+
statusListIndex: "5",
|
|
400
|
+
statusListCredential: statusListId,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const isRevoked = await manager.checkStatus(credentialStatus);
|
|
404
|
+
|
|
405
|
+
expect(isRevoked).toBe(true);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("should return false if status list doesn't exist", async () => {
|
|
409
|
+
const manager = new StatusList2021Manager(
|
|
410
|
+
mockStorage,
|
|
411
|
+
mockIdentity,
|
|
412
|
+
mockSigningFunction,
|
|
413
|
+
mockCompressor,
|
|
414
|
+
mockDecompressor
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
vi.mocked(mockStorage.getStatusList).mockResolvedValue(null);
|
|
418
|
+
|
|
419
|
+
const credentialStatus: CredentialStatus = {
|
|
420
|
+
id: "https://status.example.com/revocation/v1#5",
|
|
421
|
+
type: "StatusList2021Entry",
|
|
422
|
+
statusPurpose: "revocation",
|
|
423
|
+
statusListIndex: "5",
|
|
424
|
+
statusListCredential: "https://status.example.com/revocation/v1",
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const isRevoked = await manager.checkStatus(credentialStatus);
|
|
428
|
+
|
|
429
|
+
expect(isRevoked).toBe(false);
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
describe("getRevokedIndices", () => {
|
|
434
|
+
it("should return empty array if status list doesn't exist", async () => {
|
|
435
|
+
const manager = new StatusList2021Manager(
|
|
436
|
+
mockStorage,
|
|
437
|
+
mockIdentity,
|
|
438
|
+
mockSigningFunction,
|
|
439
|
+
mockCompressor,
|
|
440
|
+
mockDecompressor
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
vi.mocked(mockStorage.getStatusList).mockResolvedValue(null);
|
|
444
|
+
|
|
445
|
+
const indices = await manager.getRevokedIndices(
|
|
446
|
+
"https://status.example.com/revocation/v1"
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
expect(indices).toEqual([]);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("should return indices of set bits", async () => {
|
|
453
|
+
const manager = new StatusList2021Manager(
|
|
454
|
+
mockStorage,
|
|
455
|
+
mockIdentity,
|
|
456
|
+
mockSigningFunction,
|
|
457
|
+
mockCompressor,
|
|
458
|
+
mockDecompressor
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const statusListId = "https://status.example.com/revocation/v1";
|
|
462
|
+
// Create bitstring with bits 0, 2, and 5 set
|
|
463
|
+
const bytes = new Uint8Array(2);
|
|
464
|
+
bytes[0] = 0b00100101; // Bits 0, 2, 5 are set
|
|
465
|
+
const encodedList = Buffer.from(bytes).toString("base64url");
|
|
466
|
+
const existingCredential = createStatusListCredential(
|
|
467
|
+
statusListId,
|
|
468
|
+
"revocation",
|
|
469
|
+
encodedList
|
|
470
|
+
);
|
|
471
|
+
vi.mocked(mockStorage.getStatusList).mockResolvedValue(
|
|
472
|
+
existingCredential
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
const indices = await manager.getRevokedIndices(statusListId);
|
|
476
|
+
|
|
477
|
+
expect(indices).toContain(0);
|
|
478
|
+
expect(indices).toContain(2);
|
|
479
|
+
expect(indices).toContain(5);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe("createStatusListManager factory", () => {
|
|
484
|
+
it("should create a StatusList2021Manager instance", () => {
|
|
485
|
+
const manager = createStatusListManager(
|
|
486
|
+
mockStorage,
|
|
487
|
+
mockIdentity,
|
|
488
|
+
mockSigningFunction,
|
|
489
|
+
mockCompressor,
|
|
490
|
+
mockDecompressor,
|
|
491
|
+
{
|
|
492
|
+
statusListBaseUrl: "https://factory.example.com/status",
|
|
493
|
+
}
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
expect(manager).toBeInstanceOf(StatusList2021Manager);
|
|
497
|
+
expect(manager.getStatusListBaseUrl()).toBe(
|
|
498
|
+
"https://factory.example.com/status"
|
|
499
|
+
);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("should work without options", () => {
|
|
503
|
+
const manager = createStatusListManager(
|
|
504
|
+
mockStorage,
|
|
505
|
+
mockIdentity,
|
|
506
|
+
mockSigningFunction,
|
|
507
|
+
mockCompressor,
|
|
508
|
+
mockDecompressor
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
expect(manager).toBeInstanceOf(StatusList2021Manager);
|
|
512
|
+
expect(manager.getStatusListBaseUrl()).toBe("https://status.example.com");
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
});
|