first commit
This commit is contained in:
+27
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './models';
|
||||
export * from './sequence-store';
|
||||
export * from './queue-manager';
|
||||
export * from './decorators';
|
||||
export * from './config';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user