Switch to HTTP Basic Auth and improve PDF detection

- Replace token-based auth with HTTP Basic Authentication per Binect API v1 spec
- Improve PDF detection: check current tab first, then background service, fallback to recent downloads
- Add password visibility toggle in login form
- Add extensive debug logging throughout for troubleshooting
- Update manifest with alarms, activeTab permissions and <all_urls> host permission
- Add documentation files and development helper scripts
- Add Binect API specs for reference

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-14 16:50:57 +01:00
parent 0be7b56506
commit be4377253e
16 changed files with 5079 additions and 114 deletions

View File

@@ -1,18 +1,50 @@
/**
* Binect API client
* Based on Binect API v1 (Swagger spec: specs/v1_swagger_api_kernel.json)
*
* Authentication: HTTP Basic Authentication
* Base path: /binectapi/v1
*/
const API_BASE_URL = 'https://api.binect.de';
const API_BASE_URL = 'https://api.binect.de/binectapi/v1';
export interface AuthToken {
token: string;
expiresAt: string;
export interface Document {
id: number;
filename: string;
numberOfPages?: number;
status: {
code: number;
text: string;
};
documentType: 'Letter' | 'SerialLetter';
letter?: {
letterType: 'LetterData' | 'Error';
letterData?: {
recipientAddress: string;
price: {
priceBeforeTax: number;
priceAfterTax: number;
unit: string;
taxInPercent: number;
};
international: boolean;
options: Options;
};
errors?: Array<{
code: number;
text: string;
blankText: string;
}>;
};
}
export interface UploadResult {
documentId: string;
status: string;
uploadedAt: string;
export interface Options {
simplex: boolean; // if false, it's duplex
color: boolean; // if false, it's black and white
envelope?: 'DINLANG' | 'C4';
dvFranking?: boolean;
franking?: 'UNSPECIFIED' | 'STANDARD_FRANKING' | 'DV_FRANKING';
productionCountry?: 'UNSPECIFIED' | 'DE' | 'AT';
}
export class BinectAPIError extends Error {
@@ -27,95 +59,153 @@ export class BinectAPIError extends Error {
}
/**
* Authenticate with Binect API
* Create HTTP Basic Authentication header value
*/
export async function authenticate(
username: string,
password: string
): Promise<AuthToken> {
try {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
if (!response.ok) {
if (response.status === 401) {
throw new BinectAPIError('Invalid credentials', 401);
}
throw new BinectAPIError(
`Authentication failed: ${response.statusText}`,
response.status
);
}
return await response.json();
} catch (error) {
if (error instanceof BinectAPIError) {
throw error;
}
throw new BinectAPIError(
`Network error: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
function createBasicAuthHeader(username: string, password: string): string {
const credentials = `${username}:${password}`;
const base64Credentials = btoa(credentials);
return `Basic ${base64Credentials}`;
}
/**
* Upload PDF to Binect
* Convert ArrayBuffer to base64 string
*/
function arrayBufferToBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
/**
* Upload PDF to Binect API
*
* Uses HTTP Basic Authentication and uploads the PDF as base64 encoded content
* in a JSON request body according to the Binect API v1 specification.
*
* @param pdfData - PDF file as ArrayBuffer
* @param filename - Name of the PDF file
* @param username - Binect username for authentication
* @param password - Binect password for authentication
* @param options - Optional printing options (simplex/duplex, color/bw, etc.)
* @returns Document object with ID and status
*/
export async function uploadPDF(
pdfData: ArrayBuffer,
filename: string,
token: string
): Promise<UploadResult> {
try {
const formData = new FormData();
const blob = new Blob([pdfData], { type: 'application/pdf' });
formData.append('file', blob, filename);
formData.append('filename', filename);
username: string,
password: string,
options?: Options
): Promise<Document> {
console.log('[Binect API] Uploading PDF to Binect...');
console.log('[Binect API] URL:', `${API_BASE_URL}/documents`);
console.log('[Binect API] Filename:', filename);
console.log('[Binect API] PDF size:', pdfData.byteLength, 'bytes');
console.log('[Binect API] Username:', username);
const response = await fetch(`${API_BASE_URL}/documents/upload`, {
try {
// Convert PDF to base64
console.log('[Binect API] Converting PDF to base64...');
const base64Content = arrayBufferToBase64(pdfData);
console.log('[Binect API] Base64 length:', base64Content.length, 'characters');
// Prepare request body
const requestBody = {
content: {
filename,
content: base64Content
},
options: options || {
simplex: false, // duplex by default
color: false // black and white by default
}
};
console.log('[Binect API] Request options:', requestBody.options);
// Make request with HTTP Basic Authentication
const response = await fetch(`${API_BASE_URL}/documents`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`
'Content-Type': 'application/json',
'Authorization': createBasicAuthHeader(username, password)
},
body: formData
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
console.log('[Binect API] Upload response status:', response.status);
console.log('[Binect API] Response content-type:', response.headers.get('content-type'));
if (response.status === 401) {
throw new BinectAPIError('Authentication required', 401, errorData);
if (!response.ok) {
const errorText = await response.text();
console.error('[Binect API] Upload error response:', errorText);
if (response.status === 401 || response.status === 403) {
throw new BinectAPIError('Invalid credentials', response.status);
}
if (response.status === 400) {
throw new BinectAPIError(
errorData.error || 'Invalid file format',
400,
errorData
'Invalid request. Please check the PDF format and size.',
400
);
}
if (response.status === 413) {
throw new BinectAPIError('File size exceeds limit', 413, errorData);
throw new BinectAPIError('File size exceeds limit (12 MB)', 413);
}
throw new BinectAPIError(
`Upload failed: ${response.statusText}`,
response.status,
errorData
errorText
);
}
return await response.json();
const document: Document = await response.json();
console.log('[Binect API] Upload successful!');
console.log('[Binect API] Document ID:', document.id);
console.log('[Binect API] Document status:', document.status);
console.log('[Binect API] Full response:', document);
// Check if document has errors
if (document.letter?.letterType === 'Error' && document.letter.errors) {
console.warn('[Binect API] Document has errors:', document.letter.errors);
const errorMessages = document.letter.errors.map(e => e.text).join('; ');
throw new BinectAPIError(
`Document validation failed: ${errorMessages}`,
200,
document
);
}
// Status code 2 = shippable, 7 = erroneous
if (document.status.code === 7) {
console.error('[Binect API] Document is erroneous:', document.status.text);
throw new BinectAPIError(
`Document is erroneous: ${document.status.text}`,
200,
document
);
}
return document;
} catch (error) {
console.error('[Binect API] Upload error:', error);
if (error instanceof BinectAPIError) {
throw error;
}
// Check for network errors
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new BinectAPIError(
`Cannot reach Binect API at ${API_BASE_URL}. Please check your internet connection.`
);
}
throw new BinectAPIError(
`Network error: ${error instanceof Error ? error.message : 'Unknown error'}`
);
@@ -123,15 +213,40 @@ export async function uploadPDF(
}
/**
* Test API connectivity
* Test API connectivity by fetching account information
*
* @param username - Binect username
* @param password - Binect password
* @returns true if authentication successful, false otherwise
*/
export async function testConnection(): Promise<boolean> {
export async function testConnection(username: string, password: string): Promise<boolean> {
console.log('[Binect API] Testing connection to Binect API...');
console.log('[Binect API] URL:', `${API_BASE_URL}/accounts`);
try {
const response = await fetch(`${API_BASE_URL}/health`, {
method: 'GET'
const response = await fetch(`${API_BASE_URL}/accounts`, {
method: 'GET',
headers: {
'Authorization': createBasicAuthHeader(username, password)
}
});
return response.ok;
} catch {
console.log('[Binect API] Test connection response status:', response.status);
if (response.status === 401 || response.status === 403) {
console.log('[Binect API] Authentication failed');
return false;
}
if (response.ok) {
console.log('[Binect API] Connection successful');
return true;
}
console.warn('[Binect API] Unexpected response status:', response.status);
return false;
} catch (error) {
console.error('[Binect API] Connection test error:', error);
return false;
}
}