generated from coulomb/repo-seed
Fix base64 encoding for browser environment
The bufferToBase64 function from @binect/js expects Node.js Buffer objects but was receiving browser ArrayBuffer, causing "[object ArrayBuffer]" to be sent instead of valid base64. Use browser-native btoa() instead. Also updates tests to work with @binect/js integration. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,30 @@
|
||||
/**
|
||||
* Binect API client
|
||||
* Based on Binect API v1 (Swagger spec: specs/v1_swagger_api_kernel.json)
|
||||
*
|
||||
* Authentication: HTTP Basic Authentication
|
||||
* Base path: /binectapi/v1
|
||||
* This module wraps the @binect/js library to provide a simplified API
|
||||
* for the Chrome extension. It delegates all API integration to the
|
||||
* upstream library.
|
||||
*/
|
||||
|
||||
const API_BASE_URL = 'https://api.binect.de/binectapi/v1';
|
||||
import {
|
||||
BinectClient,
|
||||
BinectApiError,
|
||||
BinectAuthError,
|
||||
type Document as BinectDocument,
|
||||
type DocumentUploadOptions,
|
||||
EnvelopeType,
|
||||
FrankingType,
|
||||
} from '@binect/js';
|
||||
|
||||
// Re-export types for backward compatibility
|
||||
export interface Options {
|
||||
simplex?: boolean; // if false, it's duplex
|
||||
color?: boolean; // if false, it's black and white
|
||||
envelope?: 'DINLANG' | 'C4';
|
||||
franking?: 'UNSPECIFIED' | 'STANDARD_FRANKING' | 'DV_FRANKING';
|
||||
}
|
||||
|
||||
// Document type matching what popup.ts expects
|
||||
export interface Document {
|
||||
id: number;
|
||||
filename: string;
|
||||
@@ -38,15 +55,10 @@ export interface Document {
|
||||
};
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error class for Binect API errors
|
||||
* Wraps errors from @binect/js for backward compatibility
|
||||
*/
|
||||
export class BinectAPIError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
@@ -56,19 +68,24 @@ export class BinectAPIError extends Error {
|
||||
super(message);
|
||||
this.name = 'BinectAPIError';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a @binect/js error
|
||||
*/
|
||||
static fromBinectError(error: BinectApiError | BinectAuthError): BinectAPIError {
|
||||
if (error instanceof BinectAuthError) {
|
||||
return new BinectAPIError('Invalid credentials', 401);
|
||||
}
|
||||
return new BinectAPIError(
|
||||
error.message,
|
||||
error.status,
|
||||
error.response
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create HTTP Basic Authentication header value
|
||||
*/
|
||||
function createBasicAuthHeader(username: string, password: string): string {
|
||||
const credentials = `${username}:${password}`;
|
||||
const base64Credentials = btoa(credentials);
|
||||
return `Basic ${base64Credentials}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert ArrayBuffer to base64 string
|
||||
* Convert ArrayBuffer to base64 string (browser-compatible)
|
||||
*/
|
||||
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
@@ -79,11 +96,89 @@ function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map local Options to DocumentUploadOptions
|
||||
*/
|
||||
function mapOptions(options?: Options): Partial<DocumentUploadOptions> {
|
||||
if (!options) {
|
||||
return {
|
||||
simplex: false, // duplex by default
|
||||
color: false, // black and white by default
|
||||
};
|
||||
}
|
||||
|
||||
const mapped: Partial<DocumentUploadOptions> = {
|
||||
simplex: options.simplex ?? false,
|
||||
color: options.color ?? false,
|
||||
};
|
||||
|
||||
if (options.envelope) {
|
||||
mapped.envelope = options.envelope === 'C4' ? EnvelopeType.C4 : EnvelopeType.DINLANG;
|
||||
}
|
||||
|
||||
if (options.franking) {
|
||||
switch (options.franking) {
|
||||
case 'STANDARD_FRANKING':
|
||||
mapped.franking = FrankingType.STANDARD_FRANKING;
|
||||
break;
|
||||
case 'DV_FRANKING':
|
||||
mapped.franking = FrankingType.DV_FRANKING;
|
||||
break;
|
||||
default:
|
||||
mapped.franking = FrankingType.UNSPECIFIED;
|
||||
}
|
||||
}
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert BinectDocument to our Document interface
|
||||
*/
|
||||
function mapDocument(doc: BinectDocument): Document {
|
||||
// Build letter data if present
|
||||
let letterData: Document['letter'] = undefined;
|
||||
|
||||
if (doc.letter) {
|
||||
const ld = doc.letter.letterData;
|
||||
letterData = {
|
||||
letterType: (doc.letter.letterType || 'LetterData') as 'LetterData' | 'Error',
|
||||
letterData: ld ? {
|
||||
recipientAddress: ld.recipientAddress || '',
|
||||
price: ld.price || { priceBeforeTax: 0, priceAfterTax: 0, unit: 'EUROCENT', taxInPercent: 0 },
|
||||
international: ld.international || false,
|
||||
options: {
|
||||
simplex: ld.options?.simplex,
|
||||
color: ld.options?.color,
|
||||
envelope: ld.options?.envelope as 'DINLANG' | 'C4' | undefined,
|
||||
franking: ld.options?.franking as 'UNSPECIFIED' | 'STANDARD_FRANKING' | 'DV_FRANKING' | undefined,
|
||||
},
|
||||
} : undefined,
|
||||
errors: doc.letter.errors?.map(e => ({
|
||||
code: parseInt(e.code, 10) || 0,
|
||||
text: e.message,
|
||||
blankText: e.message,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: doc.id,
|
||||
filename: doc.filename || 'document.pdf',
|
||||
numberOfPages: doc.numberOfPages,
|
||||
status: {
|
||||
code: doc.status.code,
|
||||
text: doc.status.text,
|
||||
},
|
||||
documentType: (doc.documentType === 'SERIALLETTER' ? 'SerialLetter' : 'Letter') as 'Letter' | 'SerialLetter',
|
||||
letter: letterData,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Uses the @binect/js library to upload the PDF.
|
||||
*
|
||||
* @param pdfData - PDF file as ArrayBuffer
|
||||
* @param filename - Name of the PDF file
|
||||
@@ -99,110 +194,90 @@ export async function uploadPDF(
|
||||
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] Uploading PDF to Binect via @binect/js...');
|
||||
console.log('[Binect API] Filename:', filename);
|
||||
console.log('[Binect API] PDF size:', pdfData.byteLength, 'bytes');
|
||||
console.log('[Binect API] Username:', username);
|
||||
|
||||
try {
|
||||
// Create client with credentials
|
||||
const client = new BinectClient({
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
// 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
|
||||
}
|
||||
};
|
||||
// Map options
|
||||
const uploadOptions = mapOptions(options);
|
||||
console.log('[Binect API] Upload options:', uploadOptions);
|
||||
|
||||
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: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': createBasicAuthHeader(username, password)
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
// Upload document
|
||||
const doc = await client.documents.upload({
|
||||
content: base64Content,
|
||||
filename,
|
||||
...uploadOptions,
|
||||
});
|
||||
|
||||
console.log('[Binect API] Upload response status:', response.status);
|
||||
console.log('[Binect API] Response content-type:', response.headers.get('content-type'));
|
||||
console.log('[Binect API] Upload successful!');
|
||||
console.log('[Binect API] Document ID:', doc.id);
|
||||
console.log('[Binect API] Document status:', doc.status);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('[Binect API] Upload error response:', errorText);
|
||||
// Check if document has errors
|
||||
if (doc.letter?.letterType === 'Error' && doc.letter.errors) {
|
||||
console.warn('[Binect API] Document has errors:', doc.letter.errors);
|
||||
const errorMessages = doc.letter.errors.map(e => e.message).join('; ');
|
||||
throw new BinectAPIError(
|
||||
`Document validation failed: ${errorMessages}`,
|
||||
200,
|
||||
doc
|
||||
);
|
||||
}
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new BinectAPIError('Invalid credentials', response.status);
|
||||
}
|
||||
// Status code 7 = erroneous
|
||||
if (doc.status.code === 7) {
|
||||
console.error('[Binect API] Document is erroneous:', doc.status.text);
|
||||
throw new BinectAPIError(
|
||||
`Document is erroneous: ${doc.status.text}`,
|
||||
200,
|
||||
doc
|
||||
);
|
||||
}
|
||||
|
||||
if (response.status === 400) {
|
||||
return mapDocument(doc);
|
||||
} catch (error) {
|
||||
console.error('[Binect API] Upload error:', error);
|
||||
|
||||
// Handle @binect/js errors
|
||||
if (error instanceof BinectAuthError) {
|
||||
throw new BinectAPIError('Invalid credentials', 401);
|
||||
}
|
||||
|
||||
if (error instanceof BinectApiError) {
|
||||
// Map specific status codes
|
||||
if (error.status === 400) {
|
||||
throw new BinectAPIError(
|
||||
'Invalid request. Please check the PDF format and size.',
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
if (response.status === 413) {
|
||||
if (error.status === 413) {
|
||||
throw new BinectAPIError('File size exceeds limit (12 MB)', 413);
|
||||
}
|
||||
|
||||
throw new BinectAPIError(
|
||||
`Upload failed: ${response.statusText}`,
|
||||
response.status,
|
||||
errorText
|
||||
);
|
||||
throw BinectAPIError.fromBinectError(error);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Already a BinectAPIError
|
||||
if (error instanceof BinectAPIError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Check for network errors
|
||||
// Network or other 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.`
|
||||
'Cannot reach Binect API. Please check your internet connection.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -220,32 +295,32 @@ export async function uploadPDF(
|
||||
* @returns true if authentication successful, false otherwise
|
||||
*/
|
||||
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`);
|
||||
console.log('[Binect API] Testing connection via @binect/js...');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/accounts`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': createBasicAuthHeader(username, password)
|
||||
}
|
||||
const client = new BinectClient({
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
console.log('[Binect API] Test connection response status:', response.status);
|
||||
// Attempt to get account info
|
||||
await client.accounts.get();
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.log('[Binect API] Connection successful');
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof BinectAuthError) {
|
||||
console.log('[Binect API] Authentication failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
console.log('[Binect API] Connection successful');
|
||||
return true;
|
||||
if (error instanceof BinectApiError) {
|
||||
console.warn('[Binect API] API error:', error.message);
|
||||
// If we get any other API error, credentials might still be valid
|
||||
// but there's another issue
|
||||
return false;
|
||||
}
|
||||
|
||||
console.warn('[Binect API] Unexpected response status:', response.status);
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('[Binect API] Connection test error:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user