@ankimcp/anki-mcp-server
Version:
Model Context Protocol server for Anki - enables AI assistants to interact with your Anki flashcards
230 lines • 10.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var DeviceFlowService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.DeviceFlowService = exports.DeviceFlowError = void 0;
const common_1 = require("@nestjs/common");
const ky_1 = __importStar(require("ky"));
const app_config_service_1 = require("../app-config.service.js");
class DeviceFlowError extends Error {
code;
description;
constructor(message, code, description) {
super(message);
this.code = code;
this.description = description;
this.name = "DeviceFlowError";
}
}
exports.DeviceFlowError = DeviceFlowError;
function isDeviceFlowErrorResponse(data) {
return (typeof data === "object" &&
data !== null &&
"error" in data &&
typeof data.error === "string");
}
let DeviceFlowService = DeviceFlowService_1 = class DeviceFlowService {
config;
client;
logger = new common_1.Logger(DeviceFlowService_1.name);
deviceEndpoint;
tokenEndpoint;
constructor(config) {
this.config = config;
const tunnelUrl = this.config.tunnelServerUrl;
const httpUrl = tunnelUrl
.replace(/^wss:/, "https:")
.replace(/^ws:/, "http:");
this.deviceEndpoint = `${httpUrl}/auth/device`;
this.tokenEndpoint = `${httpUrl}/auth/token`;
this.client = ky_1.default.create({
timeout: 10000,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
retry: {
limit: 0,
},
hooks: {
beforeRequest: [
({ request }) => {
this.logger.debug(`Device Flow request: ${request.method} ${request.url}`);
},
],
},
});
}
async requestDeviceCode() {
try {
this.logger.log("Requesting device code from tunnel service");
const response = await this.client
.post(this.deviceEndpoint, {
body: new URLSearchParams({
client_id: this.config.authClientId,
scope: "openid offline_access",
}).toString(),
})
.json();
this.logger.log("Device code received successfully");
this.logger.debug(`User code: ${response.user_code}`);
return response;
}
catch (error) {
throw this.handleError(error, "requestDeviceCode");
}
}
async pollForToken(deviceCode, interval, expiresIn) {
const startTime = Date.now();
const expiresAt = startTime + expiresIn * 1000;
let currentInterval = interval * 1000;
this.logger.log("Starting token polling");
this.logger.debug(`Polling interval: ${interval}s, expires in: ${expiresIn}s`);
while (Date.now() < expiresAt) {
await this.sleep(currentInterval);
try {
const response = await this.client
.post(this.tokenEndpoint, {
timeout: 5000,
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code: deviceCode,
client_id: this.config.authClientId,
}).toString(),
})
.json();
this.logger.log("Token received successfully");
return response;
}
catch (error) {
if (error instanceof ky_1.HTTPError &&
isDeviceFlowErrorResponse(error.data)) {
const errorResponse = error.data;
switch (errorResponse.error) {
case "authorization_pending":
this.logger.debug("Authorization pending, continuing poll...");
continue;
case "slow_down":
currentInterval += 5000;
this.logger.debug(`Slow down requested, new interval: ${currentInterval / 1000}s`);
continue;
case "expired_token":
throw new DeviceFlowError("Device code has expired. Please restart the login process.", "expired_token", errorResponse.error_description);
case "access_denied":
throw new DeviceFlowError("User denied authorization.", "access_denied", errorResponse.error_description);
default:
throw new DeviceFlowError(`Authorization failed: ${errorResponse.error}`, errorResponse.error, errorResponse.error_description);
}
}
throw this.handleError(error, "pollForToken");
}
}
throw new DeviceFlowError("Polling timeout: device code expired before user authorization.", "expired_token");
}
async refreshToken(refreshToken) {
try {
this.logger.log("Refreshing access token via tunnel service");
const response = await this.client
.post(this.tokenEndpoint, {
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: this.config.authClientId,
}).toString(),
})
.json();
this.logger.log("Token refreshed successfully");
return response;
}
catch (error) {
if (error instanceof ky_1.HTTPError &&
error.response.status === 400 &&
isDeviceFlowErrorResponse(error.data) &&
error.data.error === "invalid_grant") {
throw new DeviceFlowError("Refresh token is invalid or expired. Please login again.", "invalid_grant", error.data.error_description);
}
throw this.handleError(error, "refreshToken");
}
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
handleError(error, operation) {
this.logger.error(`Error in ${operation}:`, error);
if (error instanceof ky_1.HTTPError) {
const status = error.response.status;
if (status === 401 || status === 403) {
return new DeviceFlowError("Authentication failed. Invalid client configuration.", "auth_failed");
}
if (status >= 500) {
return new DeviceFlowError("Tunnel service error. Please try again later.", "server_error");
}
return new DeviceFlowError(`HTTP error ${status}: ${error.message}`, "http_error");
}
if (error instanceof ky_1.TimeoutError) {
return new DeviceFlowError("Request timeout. Please check your network connection.", "timeout");
}
if (error instanceof Error) {
if (error.message.includes("fetch") ||
error.message.includes("network")) {
return new DeviceFlowError(`Cannot connect to tunnel service (${this.config.tunnelServerUrl}). Please check your internet connection.`, "network_error");
}
}
if (error instanceof DeviceFlowError) {
return error;
}
return new DeviceFlowError(`Unexpected error during ${operation}: ${error instanceof Error ? error.message : String(error)}`, "unknown_error");
}
getConfig() {
return {
tunnelUrl: this.config.tunnelServerUrl,
clientId: this.config.authClientId,
};
}
};
exports.DeviceFlowService = DeviceFlowService;
exports.DeviceFlowService = DeviceFlowService = DeviceFlowService_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [app_config_service_1.AppConfigService])
], DeviceFlowService);
//# sourceMappingURL=device-flow.service.js.map