first commit

This commit is contained in:
Vitalii Litvinchuk
2026-06-13 23:23:50 +03:00
commit 23958e8e2c
72 changed files with 6142 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
{
"name": "sequence-client",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sequence-client",
"version": "1.0.0",
"devDependencies": {
"typescript": "^5.4.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "sequence-client",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"dependencies": {},
"devDependencies": {
"typescript": "^5.4.0"
}
}
+9
View File
@@ -0,0 +1,9 @@
export const SequenceConfig = {
authHeader: 'X-Auth-Seq',
nextHeader: 'X-Next-Seq',
requestsRemainingHeader: 'X-Requests-Remaining'
};
export function configureSequenceAuth(config: Partial<typeof SequenceConfig>) {
Object.assign(SequenceConfig, config);
}
+56
View File
@@ -0,0 +1,56 @@
import { sequenceQueue } from './queue-manager';
import { SequenceStore } from './sequence-store';
import { SequenceState } from './models';
import { SequenceConfig } from './config';
export function SequenceProtected(customAuthHeader?: string, customNextHeader?: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
return sequenceQueue.enqueue(async () => {
const data = SequenceStore.get();
if (data.state === SequenceState.Compromised) {
throw new Error('Session is compromised. Please log in again.');
}
const initIndex = Math.max(0, originalMethod.length - 1);
let init: RequestInit = args[initIndex] || {};
const headers = new Headers(init.headers);
const authHeader = customAuthHeader || SequenceConfig.authHeader;
const nextHeader = customNextHeader || SequenceConfig.nextHeader;
if (data.state === SequenceState.Active && data.token) {
headers.set(authHeader, data.token);
}
init.headers = headers;
args[initIndex] = init;
// Execute original fetch method
const response: Response = await originalMethod.apply(this, args);
if (response.status === 401) {
SequenceStore.set({ state: SequenceState.Compromised });
throw new Error('Sequence validation failed (401). Session compromised.');
}
const remainingHeader = SequenceConfig.requestsRemainingHeader;
const nextSeq = response.headers.get(nextHeader);
const remainingStr = response.headers.get(remainingHeader);
const remaining = remainingStr ? parseInt(remainingStr, 10) : undefined;
if (nextSeq) {
SequenceStore.set({ state: SequenceState.Active, token: nextSeq, requestsRemaining: remaining });
}
return response;
});
};
return descriptor;
};
}
+5
View File
@@ -0,0 +1,5 @@
export * from './models';
export * from './sequence-store';
export * from './queue-manager';
export * from './decorators';
export * from './config';
+19
View File
@@ -0,0 +1,19 @@
export enum SequenceState {
Empty = 'Empty',
Active = 'Active',
Compromised = 'Compromised'
}
export enum QueueStatus {
Idle = 'Idle',
Processing = 'Processing',
Paused = 'Paused'
}
export type FetchStatus = 'Idle' | 'Loading' | 'Success' | 'Error';
export interface SequenceData {
state: SequenceState;
token?: string;
requestsRemaining?: number;
}
@@ -0,0 +1,47 @@
import { QueueStatus, SequenceState } from './models';
import { SequenceStore } from './sequence-store';
type Task<T> = () => Promise<T>;
export class QueueManager {
private queue: Array<{ task: Task<any>, resolve: (val: any) => void, reject: (err: any) => void }> = [];
private status: QueueStatus = QueueStatus.Idle;
async enqueue<T>(task: Task<T>): Promise<T> {
const data = SequenceStore.get();
if (data.state === SequenceState.Compromised) {
return Promise.reject(new Error('Sequence state is compromised. Re-authentication required.'));
}
return new Promise<T>((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.processNext();
});
}
private async processNext(): Promise<void> {
if (this.status !== QueueStatus.Idle) {
return;
}
const nextItem = this.queue.shift();
if (!nextItem) {
return;
}
this.status = QueueStatus.Processing;
try {
const result = await nextItem.task();
nextItem.resolve(result);
} catch (err) {
nextItem.reject(err);
} finally {
this.status = QueueStatus.Idle;
this.processNext();
}
}
}
export const sequenceQueue = new QueueManager();
@@ -0,0 +1,26 @@
import { SequenceState, SequenceData } from './models';
export class SequenceStore {
private static readonly STORAGE_KEY = 'sequence_token_data';
static get(): SequenceData {
const data = localStorage.getItem(this.STORAGE_KEY);
if (!data) {
return { state: SequenceState.Empty };
}
try {
return JSON.parse(data) as SequenceData;
} catch {
return { state: SequenceState.Empty };
}
}
static set(data: SequenceData): void {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data));
}
static clear(): void {
localStorage.removeItem(this.STORAGE_KEY);
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"experimentalDecorators": true
},
"include": ["src/**/*"]
}