bring greencheck-api-client into the app to prevent dependency issues with gitlab-runner

main
Christopher Maujean 2 months ago
parent 30412ff25f
commit bc2d9bb842
  1. 595
      bun.lock
  2. 2
      package.json
  3. 43
      src/lib/greencheck-api-client/greencheck.test.ts
  4. 193
      src/lib/greencheck-api-client/index.ts
  5. 103
      src/lib/greencheck-api-client/types.ts

File diff suppressed because it is too large Load Diff

@ -24,7 +24,7 @@
"@mui/material": "^7.2.0",
"@rdfjs/data-model": "^1.2.0",
"@rdfjs/types": "^1.0.1",
"greencheck-api-client": "git+https://gitlab.allelo.eco/nextgraph/greencheck-api-client.git",
"dotenv": "^17.1.0",
"nextgraph-react": "^0.1.1-alpha.1",
"nextgraphweb": "^0.1.1-alpha.4",
"qrcode.react": "^4.2.0",

@ -0,0 +1,43 @@
import { GreenCheckClient } from './index';
// Mock fetch globally
global.fetch = jest.fn();
describe('GreenCheckClient', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('should instantiate with valid config', () => {
const client = new GreenCheckClient({
authToken: 'test-token'
});
expect(client).toBeInstanceOf(GreenCheckClient);
});
test('should make phone verification request', async () => {
const mockResponse = { success: true };
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockResponse)
});
const client = new GreenCheckClient({
authToken: 'test-token'
});
const result = await client.requestPhoneVerification('+12345678901');
expect(result).toBe(true);
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/api/gc-mobile/start-phone-claim'),
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Authorization': 'test-token',
'Content-Type': 'application/json'
})
})
);
});
});

@ -0,0 +1,193 @@
import {
GreenCheckId,
GreenCheckClientConfig,
PhoneClaimStartResponse,
PhoneClaimValidateResponse,
GreenCheckClaim,
ClaimsResponse,
GreenCheckError,
AuthSession,
AuthenticationError,
ValidationError,
RequestOptions
} from "./types"
// Cross-platform fetch implementation
function getGlobalFetch(): typeof fetch {
if (typeof globalThis !== 'undefined' && globalThis.fetch) {
return globalThis.fetch.bind(globalThis);
}
if (typeof window !== 'undefined' && window.fetch) {
return window.fetch.bind(window);
}
if (typeof global !== 'undefined' && (global as Record<string, unknown>).fetch) {
return ((global as Record<string, unknown>).fetch as typeof fetch).bind(global);
}
// For Node.js environments without fetch polyfill
try {
const nodeFetch = require('node-fetch');
return nodeFetch.default || nodeFetch;
} catch {
throw new Error('No fetch implementation found. Please install node-fetch for Node.js environments.');
}
}
// Cross-platform AbortSignal timeout
function createTimeoutSignal(timeout: number): AbortSignal {
if (typeof AbortSignal !== 'undefined' && AbortSignal.timeout) {
return AbortSignal.timeout(timeout);
}
// Fallback for environments without AbortSignal.timeout
const controller = new AbortController();
setTimeout(() => controller.abort(), timeout);
return controller.signal;
}
export class GreenCheckClient {
private config: Required<GreenCheckClientConfig>;
private fetch: typeof fetch;
constructor(config: GreenCheckClientConfig) {
this.config = {
serverUrl: 'https://greencheck.world',
timeout: 30000,
...config
};
this.fetch = getGlobalFetch();
}
private formatPhone(phone: string): string | null {
let digits = phone.replace(/[^+\d]/g, '');
// Add country code if not present
if (!digits.startsWith('+')) {
digits = `+1${digits}`;
}
// Validate format (11+ digits with country code)
if (!/^\+\d{11,}$/.test(digits)) {
return null;
}
return digits;
}
private async makeRequest<T>(options: RequestOptions): Promise<T> {
const url = `${this.config.serverUrl}${options.endpoint}`;
const headers = {
'Authorization': this.config.authToken,
'Content-Type': 'application/json',
...options.headers
};
const fetchOptions: RequestInit = {
method: options.method,
headers,
signal: createTimeoutSignal(this.config.timeout)
};
if (options.body) {
fetchOptions.body = JSON.stringify(options.body);
}
const response = await this.fetch(url, fetchOptions);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new GreenCheckError(
error.message || `HTTP ${response.status}: ${response.statusText}`,
error.code,
response.status
);
}
return await response.json();
}
async requestPhoneVerification(phone: string): Promise<boolean> {
const formattedPhone = this.formatPhone(phone);
if (!formattedPhone) {
throw new ValidationError('Invalid phone number format. US/Canada numbers only.');
}
const response = await this.makeRequest<PhoneClaimStartResponse>({
endpoint: '/api/gc-mobile/start-phone-claim',
method: 'POST',
body: { phone: formattedPhone }
});
return response.success;
}
async verifyPhoneCode(phone: string, code: string): Promise<AuthSession> {
const formattedPhone = this.formatPhone(phone);
if (!formattedPhone) {
throw new ValidationError('Invalid phone number format.');
}
const response = await this.makeRequest<PhoneClaimValidateResponse>({
endpoint: '/api/gc-mobile/validate-phone-code',
method: 'POST',
body: { phone: formattedPhone, code }
});
if (!response.success || !response.authToken || !response.greenCheck) {
throw new AuthenticationError(response.error || 'Phone verification failed');
}
return {
authToken: response.authToken,
greenCheckId: response.greenCheck.greenCheckId
};
}
async getGreenCheckIdFromToken(authToken: string): Promise<string> {
const response = await this.makeRequest<{ greenCheck: GreenCheckId }>({
endpoint: `/api/gc-mobile/id-for-token?token=${authToken}`,
method: 'GET'
});
if (!response.greenCheck) {
throw new AuthenticationError('No GreenCheck ID found for the provided token');
}
return response.greenCheck.greenCheckId;
}
async getClaims(authToken: string): Promise<GreenCheckClaim[]> {
const greenCheckId = await this.getGreenCheckIdFromToken(authToken);
const response = await this.makeRequest<ClaimsResponse>({
endpoint: `/api/gc-mobile/claims-for-id?gcId=${greenCheckId}&token=${authToken}`,
method: 'GET'
});
return response.claims || [];
}
async generateOTT(authToken: string): Promise<string> {
const response = await this.makeRequest<{ ott: string }>({
endpoint: '/api/gc-mobile/register-ott',
method: 'POST',
body: { token: authToken }
});
return response.ott;
}
}
// Default export
export default GreenCheckClient;
export {
GreenCheckId,
GreenCheckClientConfig,
PhoneClaimStartResponse,
PhoneClaimValidateResponse,
GreenCheckClaim,
ClaimsResponse,
AuthSession
}

@ -0,0 +1,103 @@
// Core GreenCheck Identity
export interface GreenCheckId {
greenCheckId: string;
created: string; // ISO datetime
lastAccess: string; // ISO datetime
numAccesses: number;
username?: string;
}
// Claim data structure
export interface ClaimData {
id?: string | number; // provider ID
username?: string;
avatar?: string;
image?: string;
description?: string;
fullname?: string;
location?: string | null;
url?: string;
[key: string]: unknown; // Allow additional fields
}
// GreenCheck claim
export interface GreenCheckClaim {
_id: string;
greenCheckId: string;
created: string; // ISO datetime
updated?: string; // ISO datetime
provider: string;
firstClaim?: string;
numClaims?: number;
claimData: ClaimData;
}
// API Response types
export interface PhoneClaimStartResponse {
success: boolean;
error?: string;
}
export interface PhoneClaimValidateResponse {
success: boolean;
authToken?: string;
greenCheck?: GreenCheckId;
error?: string;
}
export interface ClaimsResponse {
claims: GreenCheckClaim[];
}
// Authentication session
export interface AuthSession {
authToken: string;
greenCheckId: string;
expiresAt?: Date;
}
// Client configuration
export interface GreenCheckClientConfig {
serverUrl?: string;
authToken: string;
timeout?: number;
}
// Error classes
export class GreenCheckError extends Error {
public code?: string;
public statusCode?: number;
constructor(
message: string,
code?: string,
statusCode?: number
) {
super(message);
this.name = 'GreenCheckError';
this.code = code;
this.statusCode = statusCode;
}
}
export class AuthenticationError extends GreenCheckError {
constructor(message: string) {
super(message, 'AUTHENTICATION_ERROR', 401);
this.name = 'AuthenticationError';
}
}
export class ValidationError extends GreenCheckError {
constructor(message: string) {
super(message, 'VALIDATION_ERROR', 400);
this.name = 'ValidationError';
}
}
// Cross-platform HTTP client
export interface RequestOptions {
endpoint: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: unknown;
headers?: Record<string, string>;
}
Loading…
Cancel
Save