@kya-os/mcp-i
Version:
The TypeScript MCP framework with identity features built-in
94 lines (93 loc) • 3.01 kB
JavaScript
;
/**
* Cloudflare KV Resume Token Store
*
* Durable resume-token store backed by a Cloudflare Workers KV namespace, so a
* resume token minted at consent time survives cold starts, process restarts,
* and multi-instance deployments (the failure mode the in-memory store causes —
* the user gets re-prompted because the token vanished). Mirrors the
* delegation-verifier-kv.ts adapter shape.
*
* Key structure:
* - `resume:{token}` — JSON-encoded {@link StoredResumeToken}
*
* Tokens are minted with CSPRNG entropy (see generateResumeToken in
* @kya-os/mcp-i-core), never time+Math.random.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CloudflareKVResumeTokenStore = void 0;
const mcp_i_core_1 = require("@kya-os/mcp-i-core");
// Cloudflare KV requires expirationTtl >= 60 seconds.
const MIN_KV_TTL_SECONDS = 60;
function toTtlSeconds(ms) {
return Math.max(MIN_KV_TTL_SECONDS, Math.ceil(ms / 1000));
}
class CloudflareKVResumeTokenStore {
kv;
ttlMs;
constructor(kv, ttlMs = 600_000) {
this.kv = kv;
this.ttlMs = ttlMs;
}
key(token) {
return `resume:${token}`;
}
async read(token) {
const raw = await this.kv.get(this.key(token));
if (!raw)
return null;
try {
return JSON.parse(raw);
}
catch {
return null;
}
}
async create(agentDid, scopes, metadata) {
const token = (0, mcp_i_core_1.generateResumeToken)("rt");
const now = Date.now();
const record = {
agentDid,
scopes,
createdAt: now,
expiresAt: now + this.ttlMs,
metadata,
fulfilled: false,
};
await this.kv.put(this.key(token), JSON.stringify(record), {
expirationTtl: toTtlSeconds(this.ttlMs),
});
return token;
}
async get(token) {
const record = await this.read(token);
if (!record)
return null;
if (Date.now() > record.expiresAt) {
await this.kv.delete(this.key(token));
return null;
}
if (record.fulfilled)
return null;
return {
agentDid: record.agentDid,
scopes: record.scopes,
createdAt: record.createdAt,
expiresAt: record.expiresAt,
metadata: record.metadata,
};
}
async fulfill(token) {
const record = await this.read(token);
if (!record)
return;
record.fulfilled = true;
// Preserve the original expiry window so a fulfilled token can't outlive
// its TTL; floor at the KV minimum.
const remainingMs = Math.max(0, record.expiresAt - Date.now());
await this.kv.put(this.key(token), JSON.stringify(record), {
expirationTtl: toTtlSeconds(remainingMs),
});
}
}
exports.CloudflareKVResumeTokenStore = CloudflareKVResumeTokenStore;