@ractive-ch/tab-mcp 0.3.1 → 0.4.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/README.md +2 -2
- package/exchange.d.ts +12 -3
- package/identity.d.ts +2 -1
- package/index.js +347 -123
- package/index.js.map +16 -16
- package/log.d.ts +5 -0
- package/main.js +347 -123
- package/main.js.map +16 -16
- package/notopen.d.ts +8 -2
- package/oauth/cimd.d.ts +15 -1
- package/oauth/oidc.d.ts +5 -1
- package/oauth/pairing.d.ts +2 -0
- package/oauth/server.d.ts +2 -0
- package/package.json +1 -1
- package/protocol.d.ts +1 -0
- package/ratelimit.d.ts +2 -0
- package/relay.d.ts +2 -0
- package/session.d.ts +10 -3
- package/store/memory.d.ts +1 -1
- package/store/sqlite.d.ts +1 -1
- package/store/types.d.ts +12 -4
- package/testing.js +27 -5
- package/testing.js.map +3 -3
package/notopen.d.ts
CHANGED
|
@@ -6,9 +6,11 @@ import { type RpcId } from "./exchange.js";
|
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
8
|
* Why the page is not there: never came, went away mid-request, or is open
|
|
9
|
-
* but nobody is signed in to it (identity binding, DEC-077).
|
|
9
|
+
* but nobody is signed in to it (identity binding, DEC-077). While the relay
|
|
10
|
+
* stops, `restarting` (the request never reached the page) or
|
|
11
|
+
* `restarting_gone` (it was in flight).
|
|
10
12
|
*/
|
|
11
|
-
export type NotOpenPhase = "not_open" | "gone" | "signed_out";
|
|
13
|
+
export type NotOpenPhase = "not_open" | "gone" | "signed_out" | "restarting" | "restarting_gone";
|
|
12
14
|
/** What the relay says about itself in a stand-in `server/discover` result. */
|
|
13
15
|
export interface RelayInfo {
|
|
14
16
|
name: string;
|
|
@@ -19,8 +21,12 @@ export declare function isRetryable(method: unknown): boolean;
|
|
|
19
21
|
export declare function notOpenText(appName: string | undefined): string;
|
|
20
22
|
/** The text for a page that is open but not signed in (DEC-077). */
|
|
21
23
|
export declare function signedOutText(appName: string | undefined): string;
|
|
24
|
+
/** The text while the relay stops: the AI app should try again, not send the user. */
|
|
25
|
+
export declare function restartingText(): string;
|
|
22
26
|
/** The text for a tool call whose page went away while it ran. */
|
|
23
27
|
export declare function goneDuringCallText(appName: string | undefined): string;
|
|
28
|
+
/** The text for a tool call that was running when the relay stopped. */
|
|
29
|
+
export declare function restartedDuringCallText(): string;
|
|
24
30
|
/**
|
|
25
31
|
* The JSON-RPC message the relay answers with (at HTTP 200) when the page is
|
|
26
32
|
* not open, or `undefined` for a notification (that keeps an HTTP error
|
package/oauth/cimd.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* against SSRF (threat model A3): https only, default port, no credentials,
|
|
8
8
|
* the host resolved first and every address checked to be public, no
|
|
9
9
|
* redirects, 5 s, 64 KiB, `application/json`, results cached and fetches per
|
|
10
|
-
*
|
|
10
|
+
* domain and in total rate-limited.
|
|
11
11
|
*/
|
|
12
12
|
import type { Logger } from "../log.js";
|
|
13
13
|
/** What the relay needs to know about a client. */
|
|
@@ -52,6 +52,14 @@ export interface ClientMetadataFetcherOptions {
|
|
|
52
52
|
* removed; whitespace collapses; at most 64 characters (DEC-065).
|
|
53
53
|
*/
|
|
54
54
|
export declare function displayName(raw: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* The key fetches are limited under (DEC-089): the last two labels of a host
|
|
57
|
+
* name, so `*.evil.example` is one key however many subdomains it has; an IP
|
|
58
|
+
* literal as is. Without the Public Suffix List, clients under a multi-label
|
|
59
|
+
* suffix (`co.uk`, `pages.dev`) share one key, so many hosts there can slow
|
|
60
|
+
* the others' uncached fetches; one host alone cannot (its own limit is lower).
|
|
61
|
+
*/
|
|
62
|
+
export declare function limitKey(hostname: string): string;
|
|
55
63
|
/** Fetches, validates and caches Client ID Metadata Documents. */
|
|
56
64
|
export declare class ClientMetadataFetcher {
|
|
57
65
|
#private;
|
|
@@ -59,6 +67,12 @@ export declare class ClientMetadataFetcher {
|
|
|
59
67
|
/** The client's metadata, from the cache or freshly fetched. Throws `ClientMetadataError`. */
|
|
60
68
|
get(clientId: string): Promise<ClientMetadata>;
|
|
61
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether a name contains a domain-shaped token that is not the verified
|
|
72
|
+
* host, a subdomain of it or a parent of it (DEC-092). Internationalised
|
|
73
|
+
* names are compared in their ASCII form, as the host is.
|
|
74
|
+
*/
|
|
75
|
+
export declare function foreignDomain(name: string, hostname: string): boolean;
|
|
62
76
|
/** Check a fetched document against the CIMD rules and what the relay supports. */
|
|
63
77
|
export declare function validateDocument(doc: unknown, clientId: string, url: URL): ClientMetadata;
|
|
64
78
|
/**
|
package/oauth/oidc.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface OidcPairingOptions {
|
|
|
35
35
|
publicUrl: () => string;
|
|
36
36
|
logger: Logger;
|
|
37
37
|
development: boolean;
|
|
38
|
+
/** At most this many open sign-ins (tests lower it). */
|
|
39
|
+
maxFlows?: number;
|
|
38
40
|
}
|
|
39
41
|
/** The identity-binding pairing method. */
|
|
40
42
|
export declare class OidcPairing implements PairingMethod {
|
|
@@ -47,7 +49,7 @@ export declare class OidcPairing implements PairingMethod {
|
|
|
47
49
|
/** The provider's origins, for the page's CSP `form-action` (the redirect after the post). */
|
|
48
50
|
formTargets(): string[];
|
|
49
51
|
render(target: PairingTarget): string;
|
|
50
|
-
check(target: PairingTarget, _form: URLSearchParams,
|
|
52
|
+
check(target: PairingTarget, _form: URLSearchParams, admit: () => boolean, context?: PairingContext): Promise<PairingResult>;
|
|
51
53
|
/**
|
|
52
54
|
* The provider sent the browser back. Returns the pending request the
|
|
53
55
|
* sign-in belongs to (if the state is known) and the result.
|
|
@@ -56,4 +58,6 @@ export declare class OidcPairing implements PairingMethod {
|
|
|
56
58
|
requestId?: string;
|
|
57
59
|
result: PairingResult;
|
|
58
60
|
}>;
|
|
61
|
+
/** The pending request ended: drop its sign-in and its mismatch mark. */
|
|
62
|
+
forget(requestId: string): void;
|
|
59
63
|
}
|
package/oauth/pairing.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface PairingMethod {
|
|
|
57
57
|
prepare?(): Promise<void>;
|
|
58
58
|
/** Extra origins the page's form may lead to (CSP `form-action`). */
|
|
59
59
|
formTargets?(): readonly string[];
|
|
60
|
+
/** The pending request ended (paired, cancelled, expired, evicted): drop its state. */
|
|
61
|
+
forget?(requestId: string): void;
|
|
60
62
|
/** The answer of a method that redirected (`/authorize/callback`). */
|
|
61
63
|
callback?(params: URLSearchParams, browserId: string, targetFor: (requestId: string) => PairingTarget | undefined): Promise<{
|
|
62
64
|
requestId?: string;
|
package/oauth/server.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface AuthorizationTestMode {
|
|
|
41
41
|
authCodeTtlMs?: number;
|
|
42
42
|
/** Replace the pairing methods (a stub method proves the server needs no codes). */
|
|
43
43
|
pairingMethods?: Partial<Record<PairingKind, PairingMethod>>;
|
|
44
|
+
/** A smaller cap on open OIDC sign-ins. */
|
|
45
|
+
oidcMaxFlows?: number;
|
|
44
46
|
}
|
|
45
47
|
/** BASE64URL(SHA256(verifier)) (RFC 7636 §4.6). */
|
|
46
48
|
export declare function s256(verifier: string): string;
|
package/package.json
CHANGED
package/protocol.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export declare const CloseCode: {
|
|
|
19
19
|
readonly heartbeatLost: 4003;
|
|
20
20
|
readonly shuttingDown: 4004;
|
|
21
21
|
readonly replaced: 4005;
|
|
22
|
+
/** Reserved in the protocol (`rate_limited`); the relay does not send it yet. */
|
|
22
23
|
readonly rateLimited: 4008;
|
|
23
24
|
};
|
|
24
25
|
export type ByeReason = "replaced" | "expired" | "idle" | "shutdown" | "user";
|
package/ratelimit.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export declare class RateLimiter {
|
|
|
16
16
|
constructor(options: RateLimitOptions);
|
|
17
17
|
/** Take one token for `key`; false when the bucket is empty. */
|
|
18
18
|
take(key: string, now?: number): boolean;
|
|
19
|
+
/** Whether `take` would succeed for `key` now; takes nothing. */
|
|
20
|
+
available(key: string, now?: number): boolean;
|
|
19
21
|
/** Drop buckets that are full again (nothing to remember). */
|
|
20
22
|
sweep(now?: number): void;
|
|
21
23
|
}
|
package/relay.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface RelayTestHooks extends AuthorizationTestMode {
|
|
|
35
35
|
pairingCodeTtlMs?: number;
|
|
36
36
|
/** Keys for tab assertions instead of fetching the JWKS URL. */
|
|
37
37
|
assertionKeys?: KeySource;
|
|
38
|
+
/** A smaller relay-wide replay cache for tab assertions. */
|
|
39
|
+
maxSeenAssertions?: number;
|
|
38
40
|
}
|
|
39
41
|
/** Start a relay. Returns once the server listens. */
|
|
40
42
|
export declare function createRelay(options?: CreateRelayOptions): Relay;
|
package/session.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RelayConfig, Site, SiteLimits } from "./config.js";
|
|
2
2
|
import { Exchange, type PageGoneReply, type RpcId } from "./exchange.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type Logger } from "./log.js";
|
|
4
4
|
import { PairingCodes } from "./oauth/pairing.js";
|
|
5
5
|
import { type ByeReason, type GrantRef, type Limits, type RelayMessage } from "./protocol.js";
|
|
6
6
|
import type { TokenStore } from "./store/types.js";
|
|
@@ -22,7 +22,7 @@ export interface SessionHost {
|
|
|
22
22
|
/** Called once when a session is over. */
|
|
23
23
|
released(session: TabSession): void;
|
|
24
24
|
/** Called once when a session is ready for MCP requests (DEC-074). */
|
|
25
|
-
ready
|
|
25
|
+
ready(session: TabSession): void;
|
|
26
26
|
/** The tab pressed Disconnect for one app; false if the grant is not this address's. */
|
|
27
27
|
revokeGrant(session: TabSession, grantId: string): Promise<boolean>;
|
|
28
28
|
/** The tab revokes every grant of its address ("New address"). */
|
|
@@ -34,8 +34,8 @@ export interface SessionHost {
|
|
|
34
34
|
/** Pairing code lifetime (tests shorten it). */
|
|
35
35
|
readonly pairingCodeTtlMs?: number;
|
|
36
36
|
}
|
|
37
|
+
/** One MCP request for the tab: always `POST /mcp` (protocol: tunnelled MCP requests). */
|
|
37
38
|
export interface ForwardInput {
|
|
38
|
-
method: string;
|
|
39
39
|
headers: Record<string, string>;
|
|
40
40
|
body: string;
|
|
41
41
|
rpcId: RpcId | undefined;
|
|
@@ -69,6 +69,11 @@ export declare class TabSession {
|
|
|
69
69
|
* (DEC-077); `undefined` while nobody is signed in.
|
|
70
70
|
*/
|
|
71
71
|
identity: string | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* The last assertion could not be checked (keys or store unavailable): while
|
|
74
|
+
* nobody is bound, AI apps are told to try again rather than to sign in.
|
|
75
|
+
*/
|
|
76
|
+
identityUnchecked: boolean;
|
|
72
77
|
readonly pending: Map<string, Exchange>;
|
|
73
78
|
constructor(ws: SocketLike, ip: string, origin: string, site: Site, host: SessionHost);
|
|
74
79
|
get ready(): boolean;
|
|
@@ -80,6 +85,8 @@ export declare class TabSession {
|
|
|
80
85
|
send(message: RelayMessage): boolean;
|
|
81
86
|
/** A frame arrived from the tab. */
|
|
82
87
|
handleFrame(data: string | Uint8Array): void;
|
|
88
|
+
/** The listen streams one grant has open on this session. */
|
|
89
|
+
listenStreams(grantId: string): number;
|
|
83
90
|
/** Tunnel one MCP request to the tab. Resolves with the AI app's response. */
|
|
84
91
|
forward(input: ForwardInput): Exchange;
|
|
85
92
|
/** End the session: optional `bye`, then close the socket. Idempotent. */
|
package/store/memory.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare class MemoryTokenStore implements TokenStore {
|
|
|
8
8
|
#private;
|
|
9
9
|
put(record: TokenRecord): Promise<boolean>;
|
|
10
10
|
get(hash: string): Promise<TokenRecord | undefined>;
|
|
11
|
-
markUsed(hash: string, now: number): Promise<boolean>;
|
|
11
|
+
markUsed(hash: string, now: number, keepUntil: number): Promise<boolean>;
|
|
12
12
|
revoke(selector: RevokeSelector, options: RevokeOptions): Promise<number>;
|
|
13
13
|
listGrants(addressId: string, now: number): Promise<GrantSummary[]>;
|
|
14
14
|
deleteExpired(now: number): Promise<number>;
|
package/store/sqlite.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare class SqliteTokenStore implements TokenStore {
|
|
|
9
9
|
constructor(path: string);
|
|
10
10
|
put(r: TokenRecord): Promise<boolean>;
|
|
11
11
|
get(hash: string): Promise<TokenRecord | undefined>;
|
|
12
|
-
markUsed(hash: string, now: number): Promise<boolean>;
|
|
12
|
+
markUsed(hash: string, now: number, keepUntil: number): Promise<boolean>;
|
|
13
13
|
revoke(selector: RevokeSelector, options: RevokeOptions): Promise<number>;
|
|
14
14
|
listGrants(addressId: string, now: number): Promise<GrantSummary[]>;
|
|
15
15
|
deleteExpired(now: number): Promise<number>;
|
package/store/types.d.ts
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* - `get` returns the record even when it is expired or used; the caller
|
|
12
12
|
* decides. It returns `undefined` after `revoke` or `deleteExpired` removed it.
|
|
13
13
|
* - `markUsed` is atomic: for one hash, exactly one call ever returns `true`,
|
|
14
|
-
* even when several run at the same time
|
|
14
|
+
* even when several run at the same time; that call also cuts `expiresAt`
|
|
15
|
+
* to its `keepUntil` (never extends it).
|
|
15
16
|
* - `revoke` leaves a tombstone at least until `keepUntil`: until then `put` refuses
|
|
16
17
|
* (returns `false`, stores nothing) a record of a revoked grant, or of a
|
|
17
18
|
* grant of a revoked address created at or before the revocation. Checking
|
|
@@ -75,10 +76,17 @@ export interface TokenStore {
|
|
|
75
76
|
/** The record for a token hash, or `undefined`. */
|
|
76
77
|
get(hash: string): Promise<TokenRecord | undefined>;
|
|
77
78
|
/**
|
|
78
|
-
* Mark a token as used at `now
|
|
79
|
-
*
|
|
79
|
+
* Mark a token as used at `now` and cut its `expiresAt` to `keepUntil`
|
|
80
|
+
* if that is earlier (never later), in one step. Returns `true` for the
|
|
81
|
+
* one call that marked it, `false` (changing nothing) if it was already
|
|
82
|
+
* used or does not exist. Must be atomic.
|
|
83
|
+
*
|
|
84
|
+
* The relay keeps a used refresh token only to detect its reuse, for a
|
|
85
|
+
* window (24 hours, DEC-091); the cut lets `deleteExpired` drop it then.
|
|
86
|
+
* A store that ignores `keepUntil` stays correct but keeps every used
|
|
87
|
+
* refresh token until its own expiry.
|
|
80
88
|
*/
|
|
81
|
-
markUsed(hash: string, now: number): Promise<boolean>;
|
|
89
|
+
markUsed(hash: string, now: number, keepUntil: number): Promise<boolean>;
|
|
82
90
|
/**
|
|
83
91
|
* Delete every token of a grant or of an address and leave a tombstone
|
|
84
92
|
* until `options.keepUntil`. Returns how many tokens were deleted.
|
package/testing.js
CHANGED
|
@@ -75,17 +75,39 @@ function tokenStoreConformance(name, factory) {
|
|
|
75
75
|
test("markUsed succeeds once, then fails", async () => {
|
|
76
76
|
const r = record({ kind: "refresh" });
|
|
77
77
|
await store.put(r);
|
|
78
|
-
expect(await store.markUsed(r.hash, now + 1)).toBe(true);
|
|
79
|
-
expect(await store.markUsed(r.hash, now + 2)).toBe(false);
|
|
78
|
+
expect(await store.markUsed(r.hash, now + 1, r.expiresAt)).toBe(true);
|
|
79
|
+
expect(await store.markUsed(r.hash, now + 2, r.expiresAt)).toBe(false);
|
|
80
80
|
expect((await store.get(r.hash))?.usedAt).toBe(now + 1);
|
|
81
81
|
});
|
|
82
|
+
test("markUsed cuts expiresAt to keepUntil, so deleteExpired drops the used token then", async () => {
|
|
83
|
+
const r = record({ kind: "refresh", expiresAt: now + 30 * 86400000 });
|
|
84
|
+
await store.put(r);
|
|
85
|
+
expect(await store.markUsed(r.hash, now, now + 86400000)).toBe(true);
|
|
86
|
+
const used = await store.get(r.hash);
|
|
87
|
+
expect(used?.usedAt).toBe(now);
|
|
88
|
+
expect(used?.expiresAt).toBe(now + 86400000);
|
|
89
|
+
await store.deleteExpired(now + 86400000 - 1);
|
|
90
|
+
expect(await store.get(r.hash)).toBeDefined();
|
|
91
|
+
await store.deleteExpired(now + 86400000);
|
|
92
|
+
expect(await store.get(r.hash)).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
test("markUsed never extends expiresAt, and a failed call changes nothing", async () => {
|
|
95
|
+
const r = record({ kind: "refresh", expiresAt: now + 1000 });
|
|
96
|
+
await store.put(r);
|
|
97
|
+
expect(await store.markUsed(r.hash, now, now + 86400000)).toBe(true);
|
|
98
|
+
expect((await store.get(r.hash))?.expiresAt).toBe(now + 1000);
|
|
99
|
+
expect(await store.markUsed(r.hash, now + 1, now + 10)).toBe(false);
|
|
100
|
+
const got = await store.get(r.hash);
|
|
101
|
+
expect(got?.expiresAt).toBe(now + 1000);
|
|
102
|
+
expect(got?.usedAt).toBe(now);
|
|
103
|
+
});
|
|
82
104
|
test("markUsed of an unknown hash fails", async () => {
|
|
83
|
-
expect(await store.markUsed("nope", now)).toBe(false);
|
|
105
|
+
expect(await store.markUsed("nope", now, now + 1000)).toBe(false);
|
|
84
106
|
});
|
|
85
107
|
test("concurrent markUsed calls: exactly one wins", async () => {
|
|
86
108
|
const r = record({ kind: "refresh" });
|
|
87
109
|
await store.put(r);
|
|
88
|
-
const results = await Promise.all(Array.from({ length: 10 }, (_, i) => store.markUsed(r.hash, now + i)));
|
|
110
|
+
const results = await Promise.all(Array.from({ length: 10 }, (_, i) => store.markUsed(r.hash, now + i, r.expiresAt)));
|
|
89
111
|
expect(results.filter(Boolean)).toHaveLength(1);
|
|
90
112
|
});
|
|
91
113
|
test("revoke by grant deletes only that grant's tokens", async () => {
|
|
@@ -213,5 +235,5 @@ export {
|
|
|
213
235
|
tokenStoreConformance
|
|
214
236
|
};
|
|
215
237
|
|
|
216
|
-
//# debugId=
|
|
238
|
+
//# debugId=AAFAD0267A04700B64756E2164756E21
|
|
217
239
|
//# sourceMappingURL=testing.js.map
|
package/testing.js.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/store/conformance.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import { afterEach, beforeEach, describe, expect, test } from \"bun:test\";\nimport type { TokenRecord, TokenStore } from \"./types.ts\";\n\n/**\n * The conformance suite every `TokenStore` must pass (DEC-041). Call it from a\n * `bun test` file with a factory that returns a fresh, empty store:\n *\n * ```ts\n * import { tokenStoreConformance } from \"@ractive-ch/tab-mcp/testing\";\n * tokenStoreConformance(\"RedisTokenStore\", async () => new RedisTokenStore(…));\n * ```\n */\nexport function tokenStoreConformance(\n\tname: string,\n\tfactory: () => TokenStore | Promise<TokenStore>,\n): void {\n\tdescribe(`TokenStore conformance: ${name}`, () => {\n\t\tlet store: TokenStore;\n\t\tconst now = 1_800_000_000_000;\n\t\tlet seq = 0;\n\n\t\tconst until = { now, keepUntil: now + 86_400_000 };\n\n\t\tconst record = (over: Partial<TokenRecord> = {}): TokenRecord => {\n\t\t\tseq++;\n\t\t\treturn {\n\t\t\t\thash: `hash-${seq}`,\n\t\t\t\tkind: \"access\",\n\t\t\t\tgrantId: \"g_1\",\n\t\t\t\taddressId: \"addr-a\",\n\t\t\t\tclientId: \"https://client.example/cimd\",\n\t\t\t\tclientName: \"Client\",\n\t\t\t\tscopes: [\"tools\"],\n\t\t\t\tgrantCreatedAt: now - 1000,\n\t\t\t\tissuedAt: now,\n\t\t\t\texpiresAt: now + 3_600_000,\n\t\t\t\t...over,\n\t\t\t};\n\t\t};\n\n\t\tbeforeEach(async () => {\n\t\t\tstore = await factory();\n\t\t});\n\n\t\tafterEach(async () => {\n\t\t\tawait store.close?.();\n\t\t});\n\n\t\ttest(\"put then get returns an equal record\", async () => {\n\t\t\tconst r = record({ scopes: [\"tools\", \"extra\"] });\n\t\t\texpect(await store.put(r)).toBe(true);\n\t\t\texpect(await store.get(r.hash)).toEqual(r);\n\t\t});\n\n\t\ttest(\"get of an unknown hash is undefined\", async () => {\n\t\t\texpect(await store.get(\"nope\")).toBeUndefined();\n\t\t});\n\n\t\ttest(\"put replaces the record with the same hash\", async () => {\n\t\t\tconst r = record();\n\t\t\tawait store.put(r);\n\t\t\tawait store.put({ ...r, clientName: \"Renamed\", expiresAt: now + 5 });\n\t\t\tconst got = await store.get(r.hash);\n\t\t\texpect(got?.clientName).toBe(\"Renamed\");\n\t\t\texpect(got?.expiresAt).toBe(now + 5);\n\t\t});\n\n\t\ttest(\"get returns expired and used records unchanged\", async () => {\n\t\t\tconst expired = record({ expiresAt: now - 1 });\n\t\t\tconst used = record({ kind: \"refresh\", usedAt: now - 10 });\n\t\t\tawait store.put(expired);\n\t\t\tawait store.put(used);\n\t\t\texpect(await store.get(expired.hash)).toEqual(expired);\n\t\t\texpect(await store.get(used.hash)).toEqual(used);\n\t\t});\n\n\t\ttest(\"a record without usedAt comes back without the key\", async () => {\n\t\t\tconst r = record();\n\t\t\tawait store.put(r);\n\t\t\tconst got = await store.get(r.hash);\n\t\t\texpect(got !== undefined && \"usedAt\" in got).toBe(false);\n\t\t});\n\n\t\ttest(\"the subject of an identity-bound grant round-trips (DEC-077)\", async () => {\n\t\t\tconst bound = record({ grantId: \"g_bound\", subject: \"key-of-alice\" });\n\t\t\tconst plain = record({ grantId: \"g_plain\" });\n\t\t\tawait store.put(bound);\n\t\t\tawait store.put(plain);\n\t\t\texpect(await store.get(bound.hash)).toEqual(bound);\n\t\t\tconst got = await store.get(plain.hash);\n\t\t\texpect(got !== undefined && \"subject\" in got).toBe(false);\n\t\t\tconst grants = await store.listGrants(\"addr-a\", now);\n\t\t\texpect(grants.find((g) => g.grantId === \"g_bound\")?.subject).toBe(\n\t\t\t\t\"key-of-alice\",\n\t\t\t);\n\t\t\tconst unbound = grants.find((g) => g.grantId === \"g_plain\");\n\t\t\texpect(unbound !== undefined && \"subject\" in unbound).toBe(false);\n\t\t});\n\n\t\ttest(\"markUsed succeeds once, then fails\", async () => {\n\t\t\tconst r = record({ kind: \"refresh\" });\n\t\t\tawait store.put(r);\n\t\t\texpect(await store.markUsed(r.hash, now + 1)).toBe(true);\n\t\t\texpect(await store.markUsed(r.hash, now + 2)).toBe(false);\n\t\t\texpect((await store.get(r.hash))?.usedAt).toBe(now + 1);\n\t\t});\n\n\t\ttest(\"markUsed of an unknown hash fails\", async () => {\n\t\t\texpect(await store.markUsed(\"nope\", now)).toBe(false);\n\t\t});\n\n\t\ttest(\"concurrent markUsed calls: exactly one wins\", async () => {\n\t\t\tconst r = record({ kind: \"refresh\" });\n\t\t\tawait store.put(r);\n\t\t\tconst results = await Promise.all(\n\t\t\t\tArray.from({ length: 10 }, (_, i) => store.markUsed(r.hash, now + i)),\n\t\t\t);\n\t\t\texpect(results.filter(Boolean)).toHaveLength(1);\n\t\t});\n\n\t\ttest(\"revoke by grant deletes only that grant's tokens\", async () => {\n\t\t\tconst a = record({ grantId: \"g_a\" });\n\t\t\tconst a2 = record({ grantId: \"g_a\", kind: \"refresh\" });\n\t\t\tconst b = record({ grantId: \"g_b\" });\n\t\t\tfor (const r of [a, a2, b]) await store.put(r);\n\t\t\texpect(await store.revoke({ grantId: \"g_a\" }, until)).toBe(2);\n\t\t\texpect(await store.get(a.hash)).toBeUndefined();\n\t\t\texpect(await store.get(a2.hash)).toBeUndefined();\n\t\t\texpect(await store.get(b.hash)).toEqual(b);\n\t\t});\n\n\t\ttest(\"revoke by address deletes only that address's tokens\", async () => {\n\t\t\tconst a = record({ addressId: \"addr-a\", grantId: \"g_a\" });\n\t\t\tconst b = record({ addressId: \"addr-b\", grantId: \"g_b\" });\n\t\t\tawait store.put(a);\n\t\t\tawait store.put(b);\n\t\t\texpect(await store.revoke({ addressId: \"addr-a\" }, until)).toBe(1);\n\t\t\texpect(await store.get(a.hash)).toBeUndefined();\n\t\t\texpect(await store.get(b.hash)).toEqual(b);\n\t\t});\n\n\t\ttest(\"revoke of nothing returns 0\", async () => {\n\t\t\texpect(await store.revoke({ grantId: \"none\" }, until)).toBe(0);\n\t\t});\n\n\t\ttest(\"a revoked grant gets no new tokens until its tombstone expires\", async () => {\n\t\t\tawait store.revoke({ grantId: \"g_dead\" }, until);\n\t\t\tconst late = record({ grantId: \"g_dead\", kind: \"refresh\" });\n\t\t\texpect(await store.put(late)).toBe(false);\n\t\t\texpect(await store.get(late.hash)).toBeUndefined();\n\t\t\texpect(await store.put(record({ grantId: \"g_alive\" }))).toBe(true);\n\t\t\tawait store.deleteExpired(until.keepUntil);\n\t\t\texpect(await store.put(record({ grantId: \"g_dead\" }))).toBe(true);\n\t\t});\n\n\t\ttest(\"a revoked address refuses grants created up to the revocation, not later ones\", async () => {\n\t\t\tawait store.revoke({ addressId: \"addr-a\" }, until);\n\t\t\texpect(\n\t\t\t\tawait store.put(record({ grantId: \"g_before\", grantCreatedAt: now })),\n\t\t\t).toBe(false);\n\t\t\texpect(\n\t\t\t\tawait store.put(\n\t\t\t\t\trecord({ grantId: \"g_after\", grantCreatedAt: now + 1 }),\n\t\t\t\t),\n\t\t\t).toBe(true);\n\t\t\texpect(\n\t\t\t\tawait store.put(\n\t\t\t\t\trecord({\n\t\t\t\t\t\tgrantId: \"g_elsewhere\",\n\t\t\t\t\t\taddressId: \"addr-b\",\n\t\t\t\t\t\tgrantCreatedAt: now,\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t).toBe(true);\n\t\t});\n\n\t\ttest(\"a put racing a revocation of its grant never survives it\", async () => {\n\t\t\tfor (let i = 0; i < 20; i++) {\n\t\t\t\tconst r = record({ grantId: `g_race_${i}`, kind: \"refresh\" });\n\t\t\t\tawait Promise.all([\n\t\t\t\t\tstore.put(r),\n\t\t\t\t\tstore.revoke({ grantId: r.grantId }, until),\n\t\t\t\t]);\n\t\t\t\texpect(await store.get(r.hash)).toBeUndefined();\n\t\t\t}\n\t\t});\n\n\t\ttest(\"deleteExpired removes records with expiresAt <= now only\", async () => {\n\t\t\tconst gone = record({ expiresAt: now - 1 });\n\t\t\tconst edge = record({ expiresAt: now });\n\t\t\tconst kept = record({ expiresAt: now + 1 });\n\t\t\tfor (const r of [gone, edge, kept]) await store.put(r);\n\t\t\texpect(await store.deleteExpired(now)).toBe(2);\n\t\t\texpect(await store.get(gone.hash)).toBeUndefined();\n\t\t\texpect(await store.get(edge.hash)).toBeUndefined();\n\t\t\texpect(await store.get(kept.hash)).toEqual(kept);\n\t\t});\n\n\t\ttest(\"listGrants groups live tokens of one address by grant\", async () => {\n\t\t\tawait store.put(\n\t\t\t\trecord({\n\t\t\t\t\tgrantId: \"g_new\",\n\t\t\t\t\tgrantCreatedAt: now - 10,\n\t\t\t\t\tclientName: \"New\",\n\t\t\t\t}),\n\t\t\t);\n\t\t\tawait store.put(\n\t\t\t\trecord({\n\t\t\t\t\tgrantId: \"g_old\",\n\t\t\t\t\tgrantCreatedAt: now - 500,\n\t\t\t\t\tclientName: \"Old\",\n\t\t\t\t\tscopes: [\"tools\"],\n\t\t\t\t}),\n\t\t\t);\n\t\t\tawait store.put(\n\t\t\t\trecord({\n\t\t\t\t\tgrantId: \"g_old\",\n\t\t\t\t\tkind: \"refresh\",\n\t\t\t\t\tgrantCreatedAt: now - 500,\n\t\t\t\t\tclientName: \"Old\",\n\t\t\t\t}),\n\t\t\t);\n\t\t\tawait store.put(record({ grantId: \"g_other\", addressId: \"addr-b\" }));\n\t\t\texpect(await store.listGrants(\"addr-a\", now)).toEqual([\n\t\t\t\t{\n\t\t\t\t\tgrantId: \"g_old\",\n\t\t\t\t\tclientId: \"https://client.example/cimd\",\n\t\t\t\t\tclientName: \"Old\",\n\t\t\t\t\tscopes: [\"tools\"],\n\t\t\t\t\tcreatedAt: now - 500,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tgrantId: \"g_new\",\n\t\t\t\t\tclientId: \"https://client.example/cimd\",\n\t\t\t\t\tclientName: \"New\",\n\t\t\t\t\tscopes: [\"tools\"],\n\t\t\t\t\tcreatedAt: now - 10,\n\t\t\t\t},\n\t\t\t]);\n\t\t});\n\n\t\ttest(\"listGrants leaves out grants without a usable token\", async () => {\n\t\t\tawait store.put(record({ grantId: \"g_expired\", expiresAt: now }));\n\t\t\tawait store.put(\n\t\t\t\trecord({ grantId: \"g_used\", kind: \"refresh\", usedAt: now - 1 }),\n\t\t\t);\n\t\t\tawait store.put(\n\t\t\t\trecord({ grantId: \"g_live\", kind: \"refresh\", expiresAt: now + 1 }),\n\t\t\t);\n\t\t\tconst grants = await store.listGrants(\"addr-a\", now);\n\t\t\texpect(grants.map((g) => g.grantId)).toEqual([\"g_live\"]);\n\t\t});\n\n\t\ttest(\"listGrants of an unknown address is empty\", async () => {\n\t\t\texpect(await store.listGrants(\"nobody\", now)).toEqual([]);\n\t\t});\n\n\t\ttest(\"changing a returned record does not change the store\", async () => {\n\t\t\tconst r = record();\n\t\t\tawait store.put(r);\n\t\t\tconst got = await store.get(r.hash);\n\t\t\tif (got === undefined) throw new Error(\"missing\");\n\t\t\tgot.scopes.push(\"mutated\");\n\t\t\tgot.expiresAt = 0;\n\t\t\texpect(await store.get(r.hash)).toEqual(r);\n\t\t});\n\t});\n}\n"
|
|
5
|
+
"import { afterEach, beforeEach, describe, expect, test } from \"bun:test\";\nimport type { TokenRecord, TokenStore } from \"./types.ts\";\n\n/**\n * The conformance suite every `TokenStore` must pass (DEC-041). Call it from a\n * `bun test` file with a factory that returns a fresh, empty store:\n *\n * ```ts\n * import { tokenStoreConformance } from \"@ractive-ch/tab-mcp/testing\";\n * tokenStoreConformance(\"RedisTokenStore\", async () => new RedisTokenStore(…));\n * ```\n */\nexport function tokenStoreConformance(\n\tname: string,\n\tfactory: () => TokenStore | Promise<TokenStore>,\n): void {\n\tdescribe(`TokenStore conformance: ${name}`, () => {\n\t\tlet store: TokenStore;\n\t\tconst now = 1_800_000_000_000;\n\t\tlet seq = 0;\n\n\t\tconst until = { now, keepUntil: now + 86_400_000 };\n\n\t\tconst record = (over: Partial<TokenRecord> = {}): TokenRecord => {\n\t\t\tseq++;\n\t\t\treturn {\n\t\t\t\thash: `hash-${seq}`,\n\t\t\t\tkind: \"access\",\n\t\t\t\tgrantId: \"g_1\",\n\t\t\t\taddressId: \"addr-a\",\n\t\t\t\tclientId: \"https://client.example/cimd\",\n\t\t\t\tclientName: \"Client\",\n\t\t\t\tscopes: [\"tools\"],\n\t\t\t\tgrantCreatedAt: now - 1000,\n\t\t\t\tissuedAt: now,\n\t\t\t\texpiresAt: now + 3_600_000,\n\t\t\t\t...over,\n\t\t\t};\n\t\t};\n\n\t\tbeforeEach(async () => {\n\t\t\tstore = await factory();\n\t\t});\n\n\t\tafterEach(async () => {\n\t\t\tawait store.close?.();\n\t\t});\n\n\t\ttest(\"put then get returns an equal record\", async () => {\n\t\t\tconst r = record({ scopes: [\"tools\", \"extra\"] });\n\t\t\texpect(await store.put(r)).toBe(true);\n\t\t\texpect(await store.get(r.hash)).toEqual(r);\n\t\t});\n\n\t\ttest(\"get of an unknown hash is undefined\", async () => {\n\t\t\texpect(await store.get(\"nope\")).toBeUndefined();\n\t\t});\n\n\t\ttest(\"put replaces the record with the same hash\", async () => {\n\t\t\tconst r = record();\n\t\t\tawait store.put(r);\n\t\t\tawait store.put({ ...r, clientName: \"Renamed\", expiresAt: now + 5 });\n\t\t\tconst got = await store.get(r.hash);\n\t\t\texpect(got?.clientName).toBe(\"Renamed\");\n\t\t\texpect(got?.expiresAt).toBe(now + 5);\n\t\t});\n\n\t\ttest(\"get returns expired and used records unchanged\", async () => {\n\t\t\tconst expired = record({ expiresAt: now - 1 });\n\t\t\tconst used = record({ kind: \"refresh\", usedAt: now - 10 });\n\t\t\tawait store.put(expired);\n\t\t\tawait store.put(used);\n\t\t\texpect(await store.get(expired.hash)).toEqual(expired);\n\t\t\texpect(await store.get(used.hash)).toEqual(used);\n\t\t});\n\n\t\ttest(\"a record without usedAt comes back without the key\", async () => {\n\t\t\tconst r = record();\n\t\t\tawait store.put(r);\n\t\t\tconst got = await store.get(r.hash);\n\t\t\texpect(got !== undefined && \"usedAt\" in got).toBe(false);\n\t\t});\n\n\t\ttest(\"the subject of an identity-bound grant round-trips (DEC-077)\", async () => {\n\t\t\tconst bound = record({ grantId: \"g_bound\", subject: \"key-of-alice\" });\n\t\t\tconst plain = record({ grantId: \"g_plain\" });\n\t\t\tawait store.put(bound);\n\t\t\tawait store.put(plain);\n\t\t\texpect(await store.get(bound.hash)).toEqual(bound);\n\t\t\tconst got = await store.get(plain.hash);\n\t\t\texpect(got !== undefined && \"subject\" in got).toBe(false);\n\t\t\tconst grants = await store.listGrants(\"addr-a\", now);\n\t\t\texpect(grants.find((g) => g.grantId === \"g_bound\")?.subject).toBe(\n\t\t\t\t\"key-of-alice\",\n\t\t\t);\n\t\t\tconst unbound = grants.find((g) => g.grantId === \"g_plain\");\n\t\t\texpect(unbound !== undefined && \"subject\" in unbound).toBe(false);\n\t\t});\n\n\t\ttest(\"markUsed succeeds once, then fails\", async () => {\n\t\t\tconst r = record({ kind: \"refresh\" });\n\t\t\tawait store.put(r);\n\t\t\texpect(await store.markUsed(r.hash, now + 1, r.expiresAt)).toBe(true);\n\t\t\texpect(await store.markUsed(r.hash, now + 2, r.expiresAt)).toBe(false);\n\t\t\texpect((await store.get(r.hash))?.usedAt).toBe(now + 1);\n\t\t});\n\n\t\ttest(\"markUsed cuts expiresAt to keepUntil, so deleteExpired drops the used token then\", async () => {\n\t\t\tconst r = record({ kind: \"refresh\", expiresAt: now + 30 * 86_400_000 });\n\t\t\tawait store.put(r);\n\t\t\texpect(await store.markUsed(r.hash, now, now + 86_400_000)).toBe(true);\n\t\t\tconst used = await store.get(r.hash);\n\t\t\texpect(used?.usedAt).toBe(now);\n\t\t\texpect(used?.expiresAt).toBe(now + 86_400_000);\n\t\t\tawait store.deleteExpired(now + 86_400_000 - 1);\n\t\t\texpect(await store.get(r.hash)).toBeDefined();\n\t\t\tawait store.deleteExpired(now + 86_400_000);\n\t\t\texpect(await store.get(r.hash)).toBeUndefined();\n\t\t});\n\n\t\ttest(\"markUsed never extends expiresAt, and a failed call changes nothing\", async () => {\n\t\t\tconst r = record({ kind: \"refresh\", expiresAt: now + 1000 });\n\t\t\tawait store.put(r);\n\t\t\texpect(await store.markUsed(r.hash, now, now + 86_400_000)).toBe(true);\n\t\t\texpect((await store.get(r.hash))?.expiresAt).toBe(now + 1000);\n\t\t\texpect(await store.markUsed(r.hash, now + 1, now + 10)).toBe(false);\n\t\t\tconst got = await store.get(r.hash);\n\t\t\texpect(got?.expiresAt).toBe(now + 1000);\n\t\t\texpect(got?.usedAt).toBe(now);\n\t\t});\n\n\t\ttest(\"markUsed of an unknown hash fails\", async () => {\n\t\t\texpect(await store.markUsed(\"nope\", now, now + 1000)).toBe(false);\n\t\t});\n\n\t\ttest(\"concurrent markUsed calls: exactly one wins\", async () => {\n\t\t\tconst r = record({ kind: \"refresh\" });\n\t\t\tawait store.put(r);\n\t\t\tconst results = await Promise.all(\n\t\t\t\tArray.from({ length: 10 }, (_, i) =>\n\t\t\t\t\tstore.markUsed(r.hash, now + i, r.expiresAt),\n\t\t\t\t),\n\t\t\t);\n\t\t\texpect(results.filter(Boolean)).toHaveLength(1);\n\t\t});\n\n\t\ttest(\"revoke by grant deletes only that grant's tokens\", async () => {\n\t\t\tconst a = record({ grantId: \"g_a\" });\n\t\t\tconst a2 = record({ grantId: \"g_a\", kind: \"refresh\" });\n\t\t\tconst b = record({ grantId: \"g_b\" });\n\t\t\tfor (const r of [a, a2, b]) await store.put(r);\n\t\t\texpect(await store.revoke({ grantId: \"g_a\" }, until)).toBe(2);\n\t\t\texpect(await store.get(a.hash)).toBeUndefined();\n\t\t\texpect(await store.get(a2.hash)).toBeUndefined();\n\t\t\texpect(await store.get(b.hash)).toEqual(b);\n\t\t});\n\n\t\ttest(\"revoke by address deletes only that address's tokens\", async () => {\n\t\t\tconst a = record({ addressId: \"addr-a\", grantId: \"g_a\" });\n\t\t\tconst b = record({ addressId: \"addr-b\", grantId: \"g_b\" });\n\t\t\tawait store.put(a);\n\t\t\tawait store.put(b);\n\t\t\texpect(await store.revoke({ addressId: \"addr-a\" }, until)).toBe(1);\n\t\t\texpect(await store.get(a.hash)).toBeUndefined();\n\t\t\texpect(await store.get(b.hash)).toEqual(b);\n\t\t});\n\n\t\ttest(\"revoke of nothing returns 0\", async () => {\n\t\t\texpect(await store.revoke({ grantId: \"none\" }, until)).toBe(0);\n\t\t});\n\n\t\ttest(\"a revoked grant gets no new tokens until its tombstone expires\", async () => {\n\t\t\tawait store.revoke({ grantId: \"g_dead\" }, until);\n\t\t\tconst late = record({ grantId: \"g_dead\", kind: \"refresh\" });\n\t\t\texpect(await store.put(late)).toBe(false);\n\t\t\texpect(await store.get(late.hash)).toBeUndefined();\n\t\t\texpect(await store.put(record({ grantId: \"g_alive\" }))).toBe(true);\n\t\t\tawait store.deleteExpired(until.keepUntil);\n\t\t\texpect(await store.put(record({ grantId: \"g_dead\" }))).toBe(true);\n\t\t});\n\n\t\ttest(\"a revoked address refuses grants created up to the revocation, not later ones\", async () => {\n\t\t\tawait store.revoke({ addressId: \"addr-a\" }, until);\n\t\t\texpect(\n\t\t\t\tawait store.put(record({ grantId: \"g_before\", grantCreatedAt: now })),\n\t\t\t).toBe(false);\n\t\t\texpect(\n\t\t\t\tawait store.put(\n\t\t\t\t\trecord({ grantId: \"g_after\", grantCreatedAt: now + 1 }),\n\t\t\t\t),\n\t\t\t).toBe(true);\n\t\t\texpect(\n\t\t\t\tawait store.put(\n\t\t\t\t\trecord({\n\t\t\t\t\t\tgrantId: \"g_elsewhere\",\n\t\t\t\t\t\taddressId: \"addr-b\",\n\t\t\t\t\t\tgrantCreatedAt: now,\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t).toBe(true);\n\t\t});\n\n\t\ttest(\"a put racing a revocation of its grant never survives it\", async () => {\n\t\t\tfor (let i = 0; i < 20; i++) {\n\t\t\t\tconst r = record({ grantId: `g_race_${i}`, kind: \"refresh\" });\n\t\t\t\tawait Promise.all([\n\t\t\t\t\tstore.put(r),\n\t\t\t\t\tstore.revoke({ grantId: r.grantId }, until),\n\t\t\t\t]);\n\t\t\t\texpect(await store.get(r.hash)).toBeUndefined();\n\t\t\t}\n\t\t});\n\n\t\ttest(\"deleteExpired removes records with expiresAt <= now only\", async () => {\n\t\t\tconst gone = record({ expiresAt: now - 1 });\n\t\t\tconst edge = record({ expiresAt: now });\n\t\t\tconst kept = record({ expiresAt: now + 1 });\n\t\t\tfor (const r of [gone, edge, kept]) await store.put(r);\n\t\t\texpect(await store.deleteExpired(now)).toBe(2);\n\t\t\texpect(await store.get(gone.hash)).toBeUndefined();\n\t\t\texpect(await store.get(edge.hash)).toBeUndefined();\n\t\t\texpect(await store.get(kept.hash)).toEqual(kept);\n\t\t});\n\n\t\ttest(\"listGrants groups live tokens of one address by grant\", async () => {\n\t\t\tawait store.put(\n\t\t\t\trecord({\n\t\t\t\t\tgrantId: \"g_new\",\n\t\t\t\t\tgrantCreatedAt: now - 10,\n\t\t\t\t\tclientName: \"New\",\n\t\t\t\t}),\n\t\t\t);\n\t\t\tawait store.put(\n\t\t\t\trecord({\n\t\t\t\t\tgrantId: \"g_old\",\n\t\t\t\t\tgrantCreatedAt: now - 500,\n\t\t\t\t\tclientName: \"Old\",\n\t\t\t\t\tscopes: [\"tools\"],\n\t\t\t\t}),\n\t\t\t);\n\t\t\tawait store.put(\n\t\t\t\trecord({\n\t\t\t\t\tgrantId: \"g_old\",\n\t\t\t\t\tkind: \"refresh\",\n\t\t\t\t\tgrantCreatedAt: now - 500,\n\t\t\t\t\tclientName: \"Old\",\n\t\t\t\t}),\n\t\t\t);\n\t\t\tawait store.put(record({ grantId: \"g_other\", addressId: \"addr-b\" }));\n\t\t\texpect(await store.listGrants(\"addr-a\", now)).toEqual([\n\t\t\t\t{\n\t\t\t\t\tgrantId: \"g_old\",\n\t\t\t\t\tclientId: \"https://client.example/cimd\",\n\t\t\t\t\tclientName: \"Old\",\n\t\t\t\t\tscopes: [\"tools\"],\n\t\t\t\t\tcreatedAt: now - 500,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tgrantId: \"g_new\",\n\t\t\t\t\tclientId: \"https://client.example/cimd\",\n\t\t\t\t\tclientName: \"New\",\n\t\t\t\t\tscopes: [\"tools\"],\n\t\t\t\t\tcreatedAt: now - 10,\n\t\t\t\t},\n\t\t\t]);\n\t\t});\n\n\t\ttest(\"listGrants leaves out grants without a usable token\", async () => {\n\t\t\tawait store.put(record({ grantId: \"g_expired\", expiresAt: now }));\n\t\t\tawait store.put(\n\t\t\t\trecord({ grantId: \"g_used\", kind: \"refresh\", usedAt: now - 1 }),\n\t\t\t);\n\t\t\tawait store.put(\n\t\t\t\trecord({ grantId: \"g_live\", kind: \"refresh\", expiresAt: now + 1 }),\n\t\t\t);\n\t\t\tconst grants = await store.listGrants(\"addr-a\", now);\n\t\t\texpect(grants.map((g) => g.grantId)).toEqual([\"g_live\"]);\n\t\t});\n\n\t\ttest(\"listGrants of an unknown address is empty\", async () => {\n\t\t\texpect(await store.listGrants(\"nobody\", now)).toEqual([]);\n\t\t});\n\n\t\ttest(\"changing a returned record does not change the store\", async () => {\n\t\t\tconst r = record();\n\t\t\tawait store.put(r);\n\t\t\tconst got = await store.get(r.hash);\n\t\t\tif (got === undefined) throw new Error(\"missing\");\n\t\t\tgot.scopes.push(\"mutated\");\n\t\t\tgot.expiresAt = 0;\n\t\t\texpect(await store.get(r.hash)).toEqual(r);\n\t\t});\n\t});\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "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
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "AAFAD0267A04700B64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|