From f2c5893004aec7fdb0e96f92357e3f7fffafe4fd Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Fri, 21 Aug 2026 08:10:12 +0400 Subject: [PATCH] release: @marketplaces/auth 0.1.0 (5ffc1b1) --- dist/.npmignore | 2 + dist/config.d.ts | 5 - dist/config.js | 5 - dist/ed25519/auth-api.service.d.ts | 17 - dist/ed25519/auth-api.service.js | 38 - dist/ed25519/auth-facade.service.d.ts | 22 - dist/ed25519/auth-facade.service.js | 60 - dist/ed25519/auth.service.d.ts | 35 - dist/ed25519/auth.service.js | 107 -- dist/ed25519/ed25519-keypair.service.d.ts | 15 - dist/ed25519/ed25519-keypair.service.js | 114 -- dist/ed25519/ed25519-verification.model.d.ts | 27 - dist/ed25519/ed25519-verification.model.js | 2 - dist/ed25519/jwt.service.d.ts | 14 - dist/ed25519/jwt.service.js | 48 - dist/ed25519/models/auth-api.model.d.ts | 36 - dist/ed25519/models/auth-api.model.js | 1 - dist/ed25519/models/auth-error.model.d.ts | 15 - dist/ed25519/models/auth-error.model.js | 24 - dist/ed25519/models/permission.model.d.ts | 10 - dist/ed25519/models/permission.model.js | 7 - .../noop-ed25519-verification.service.d.ts | 12 - .../noop-ed25519-verification.service.js | 26 - dist/ed25519/permission.service.d.ts | 12 - dist/ed25519/permission.service.js | 33 - dist/ed25519/session.service.d.ts | 35 - dist/ed25519/session.service.js | 120 -- dist/fesm2022/marketplaces-auth.mjs | 1357 +++++++++++++++++ dist/fesm2022/marketplaces-auth.mjs.map | 1 + dist/index.d.ts | 21 - dist/index.js | 24 - dist/package.json | 36 + .../admin-auth-headers.interceptor.d.ts | 7 - .../admin-auth-headers.interceptor.js | 26 - dist/telegram/admin-auth.guard.d.ts | 3 - dist/telegram/admin-auth.guard.js | 11 - dist/telegram/admin-auth.service.d.ts | 44 - dist/telegram/admin-auth.service.js | 188 --- dist/telegram/auth.service.d.ts | 49 - dist/telegram/auth.service.js | 161 -- dist/telegram/models/session.model.d.ts | 14 - dist/telegram/models/session.model.js | 1 - .../telegram-session-api.service.d.ts | 28 - dist/telegram/telegram-session-api.service.js | 137 -- dist/types/marketplaces-auth.d.ts | 545 +++++++ dist/util/guid.util.d.ts | 2 - dist/util/guid.util.js | 19 - package.json | 18 +- 48 files changed, 1956 insertions(+), 1578 deletions(-) create mode 100644 dist/.npmignore delete mode 100644 dist/config.d.ts delete mode 100644 dist/config.js delete mode 100644 dist/ed25519/auth-api.service.d.ts delete mode 100644 dist/ed25519/auth-api.service.js delete mode 100644 dist/ed25519/auth-facade.service.d.ts delete mode 100644 dist/ed25519/auth-facade.service.js delete mode 100644 dist/ed25519/auth.service.d.ts delete mode 100644 dist/ed25519/auth.service.js delete mode 100644 dist/ed25519/ed25519-keypair.service.d.ts delete mode 100644 dist/ed25519/ed25519-keypair.service.js delete mode 100644 dist/ed25519/ed25519-verification.model.d.ts delete mode 100644 dist/ed25519/ed25519-verification.model.js delete mode 100644 dist/ed25519/jwt.service.d.ts delete mode 100644 dist/ed25519/jwt.service.js delete mode 100644 dist/ed25519/models/auth-api.model.d.ts delete mode 100644 dist/ed25519/models/auth-api.model.js delete mode 100644 dist/ed25519/models/auth-error.model.d.ts delete mode 100644 dist/ed25519/models/auth-error.model.js delete mode 100644 dist/ed25519/models/permission.model.d.ts delete mode 100644 dist/ed25519/models/permission.model.js delete mode 100644 dist/ed25519/noop-ed25519-verification.service.d.ts delete mode 100644 dist/ed25519/noop-ed25519-verification.service.js delete mode 100644 dist/ed25519/permission.service.d.ts delete mode 100644 dist/ed25519/permission.service.js delete mode 100644 dist/ed25519/session.service.d.ts delete mode 100644 dist/ed25519/session.service.js create mode 100644 dist/fesm2022/marketplaces-auth.mjs create mode 100644 dist/fesm2022/marketplaces-auth.mjs.map delete mode 100644 dist/index.d.ts delete mode 100644 dist/index.js create mode 100644 dist/package.json delete mode 100644 dist/telegram/admin-auth-headers.interceptor.d.ts delete mode 100644 dist/telegram/admin-auth-headers.interceptor.js delete mode 100644 dist/telegram/admin-auth.guard.d.ts delete mode 100644 dist/telegram/admin-auth.guard.js delete mode 100644 dist/telegram/admin-auth.service.d.ts delete mode 100644 dist/telegram/admin-auth.service.js delete mode 100644 dist/telegram/auth.service.d.ts delete mode 100644 dist/telegram/auth.service.js delete mode 100644 dist/telegram/models/session.model.d.ts delete mode 100644 dist/telegram/models/session.model.js delete mode 100644 dist/telegram/telegram-session-api.service.d.ts delete mode 100644 dist/telegram/telegram-session-api.service.js create mode 100644 dist/types/marketplaces-auth.d.ts delete mode 100644 dist/util/guid.util.d.ts delete mode 100644 dist/util/guid.util.js diff --git a/dist/.npmignore b/dist/.npmignore new file mode 100644 index 0000000..c97ccf2 --- /dev/null +++ b/dist/.npmignore @@ -0,0 +1,2 @@ +# Nested package.json's are only needed for development. +**/package.json \ No newline at end of file diff --git a/dist/config.d.ts b/dist/config.d.ts deleted file mode 100644 index ffe5b56..0000000 --- a/dist/config.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { InjectionToken } from '@angular/core'; -/** Base URL for the auth backend, e.g. `https://api.example.com`. Provide from the consuming app's environment config. */ -export declare const AUTH_API_URL: InjectionToken; -/** Telegram bot username used to build QR/deep-link login URLs. Optional — falls back to a default if not provided. */ -export declare const TELEGRAM_BOT_USERNAME: InjectionToken; diff --git a/dist/config.js b/dist/config.js deleted file mode 100644 index 847d624..0000000 --- a/dist/config.js +++ /dev/null @@ -1,5 +0,0 @@ -import { InjectionToken } from '@angular/core'; -/** Base URL for the auth backend, e.g. `https://api.example.com`. Provide from the consuming app's environment config. */ -export const AUTH_API_URL = new InjectionToken('@marketplaces/auth AUTH_API_URL'); -/** Telegram bot username used to build QR/deep-link login URLs. Optional — falls back to a default if not provided. */ -export const TELEGRAM_BOT_USERNAME = new InjectionToken('@marketplaces/auth TELEGRAM_BOT_USERNAME'); diff --git a/dist/ed25519/auth-api.service.d.ts b/dist/ed25519/auth-api.service.d.ts deleted file mode 100644 index e94c306..0000000 --- a/dist/ed25519/auth-api.service.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Observable } from 'rxjs'; -import { AuthChallenge, AuthTokenPair, RefreshTokenRequest, VerifySignatureRequest } from './models/auth-api.model'; -/** - * Thin HTTP client for the Ed25519 admin auth endpoints. These endpoints may - * not exist on every backend yet - calling them before the backend ships - * 404s or connection-errors, which AuthService maps to the - * `backend-unavailable` error screen. No mock/fake responses are fabricated - * here; this is real HttpClient wiring against the real contract. - */ -export declare class AuthApiService { - private readonly http; - private readonly baseUrl; - requestChallenge(): Observable; - verifySignature(request: VerifySignatureRequest): Observable; - refresh(request: RefreshTokenRequest): Observable; - logout(refreshToken: string): Observable; -} diff --git a/dist/ed25519/auth-api.service.js b/dist/ed25519/auth-api.service.js deleted file mode 100644 index efebb56..0000000 --- a/dist/ed25519/auth-api.service.js +++ /dev/null @@ -1,38 +0,0 @@ -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -import { HttpClient } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; -import { AUTH_API_URL } from '../config'; -/** - * Thin HTTP client for the Ed25519 admin auth endpoints. These endpoints may - * not exist on every backend yet - calling them before the backend ships - * 404s or connection-errors, which AuthService maps to the - * `backend-unavailable` error screen. No mock/fake responses are fabricated - * here; this is real HttpClient wiring against the real contract. - */ -let AuthApiService = class AuthApiService { - constructor() { - this.http = inject(HttpClient); - this.baseUrl = `${inject(AUTH_API_URL)}/api/admin/auth`; - } - requestChallenge() { - return this.http.get(`${this.baseUrl}/challenge`); - } - verifySignature(request) { - return this.http.post(`${this.baseUrl}/verify`, request); - } - refresh(request) { - return this.http.post(`${this.baseUrl}/refresh`, request); - } - logout(refreshToken) { - return this.http.post(`${this.baseUrl}/logout`, { refreshToken }); - } -}; -AuthApiService = __decorate([ - Injectable({ providedIn: 'root' }) -], AuthApiService); -export { AuthApiService }; diff --git a/dist/ed25519/auth-facade.service.d.ts b/dist/ed25519/auth-facade.service.d.ts deleted file mode 100644 index 359f2bc..0000000 --- a/dist/ed25519/auth-facade.service.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Permission } from './models/permission.model'; -/** - * Public surface for components/pages. Components should depend on this, - * not on AuthService/SessionService/PermissionService directly, so the - * orchestration details (which service owns what) can change without - * touching UI code. - */ -export declare class AuthFacade { - private readonly auth; - private readonly session; - private readonly permissions; - private readonly router; - readonly isAuthenticated: import("@angular/core").Signal; - readonly status: import("@angular/core").Signal; - readonly role: import("@angular/core").Signal; - readonly loginPhase: import("@angular/core").Signal; - readonly lastError: import("@angular/core").Signal; - restoreSession(): void; - login(onSuccessRedirectTo?: string): void; - logout(redirectTo?: string): void; - can(permission: Permission): boolean; -} diff --git a/dist/ed25519/auth-facade.service.js b/dist/ed25519/auth-facade.service.js deleted file mode 100644 index ef045d2..0000000 --- a/dist/ed25519/auth-facade.service.js +++ /dev/null @@ -1,60 +0,0 @@ -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -import { Injectable, inject } from '@angular/core'; -import { Router } from '@angular/router'; -import { finalize } from 'rxjs'; -import { AuthService } from './auth.service'; -import { PermissionService } from './permission.service'; -import { SessionService } from './session.service'; -/** - * Public surface for components/pages. Components should depend on this, - * not on AuthService/SessionService/PermissionService directly, so the - * orchestration details (which service owns what) can change without - * touching UI code. - */ -let AuthFacade = class AuthFacade { - constructor() { - this.auth = inject(AuthService); - this.session = inject(SessionService); - this.permissions = inject(PermissionService); - this.router = inject(Router); - this.isAuthenticated = this.session.isAuthenticated; - this.status = this.session.status; - this.role = this.session.role; - this.loginPhase = this.auth.loginPhase; - this.lastError = this.auth.lastError; - } - restoreSession() { - this.auth.restoreSession(); - } - login(onSuccessRedirectTo) { - this.auth.login().subscribe({ - next: () => { - if (onSuccessRedirectTo) { - this.router.navigateByUrl(onSuccessRedirectTo); - } - }, - error: () => { - const code = this.auth.lastError()?.code ?? 'unauthorized'; - this.router.navigate(['/admin-login/error', code]); - } - }); - } - logout(redirectTo = '/admin-login') { - this.auth - .logout() - .pipe(finalize(() => this.router.navigateByUrl(redirectTo))) - .subscribe({ error: () => undefined }); - } - can(permission) { - return this.permissions.has(permission); - } -}; -AuthFacade = __decorate([ - Injectable({ providedIn: 'root' }) -], AuthFacade); -export { AuthFacade }; diff --git a/dist/ed25519/auth.service.d.ts b/dist/ed25519/auth.service.d.ts deleted file mode 100644 index 5049a31..0000000 --- a/dist/ed25519/auth.service.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Observable } from 'rxjs'; -import { AuthTokenPair } from './models/auth-api.model'; -import { AuthError } from './models/auth-error.model'; -export type LoginPhase = 'idle' | 'requesting-challenge' | 'signing' | 'verifying' | 'done'; -/** - * Orchestrates the Ed25519 challenge/response admin auth flow end to end: - * - * GET /api/admin/auth/challenge -> { nonce } - * sign(nonce) with local Ed25519 key -> signature - * POST /api/admin/auth/verify -> { token, refreshToken } - * - * This is the lowest-level orchestrator; components should go through - * AuthFacade rather than calling this directly. Exported from the package - * barrel as `Ed25519AuthService` to avoid colliding with the telegram - * module's `AuthService`. - */ -export declare class AuthService { - private readonly api; - private readonly keypair; - private readonly session; - private readonly loginPhaseSignal; - private readonly lastErrorSignal; - readonly loginPhase: import("@angular/core").Signal; - readonly lastError: import("@angular/core").Signal; - constructor(); - /** Restores a persisted session on app bootstrap. Call once from an APP_INITIALIZER or root component. */ - restoreSession(): void; - login(): Observable; - refresh(): Observable; - logout(): Observable; - private signChallenge; - private handleAuthError; - private toAuthErrorShape; - private toAuthError; -} diff --git a/dist/ed25519/auth.service.js b/dist/ed25519/auth.service.js deleted file mode 100644 index a5b4023..0000000 --- a/dist/ed25519/auth.service.js +++ /dev/null @@ -1,107 +0,0 @@ -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -import { Injectable, inject, signal } from '@angular/core'; -import { HttpErrorResponse } from '@angular/common/http'; -import { catchError, switchMap, tap, throwError } from 'rxjs'; -import { Observable } from 'rxjs'; -import { authErrorCodeFromBackendCode, authErrorCodeFromStatus } from './models/auth-error.model'; -import { AuthApiService } from './auth-api.service'; -import { Ed25519KeypairService } from './ed25519-keypair.service'; -import { SessionService } from './session.service'; -/** - * Orchestrates the Ed25519 challenge/response admin auth flow end to end: - * - * GET /api/admin/auth/challenge -> { nonce } - * sign(nonce) with local Ed25519 key -> signature - * POST /api/admin/auth/verify -> { token, refreshToken } - * - * This is the lowest-level orchestrator; components should go through - * AuthFacade rather than calling this directly. Exported from the package - * barrel as `Ed25519AuthService` to avoid colliding with the telegram - * module's `AuthService`. - */ -let AuthService = class AuthService { - constructor() { - this.api = inject(AuthApiService); - this.keypair = inject(Ed25519KeypairService); - this.session = inject(SessionService); - this.loginPhaseSignal = signal('idle'); - this.lastErrorSignal = signal(null); - this.loginPhase = this.loginPhaseSignal.asReadonly(); - this.lastError = this.lastErrorSignal.asReadonly(); - this.session.onRefreshDue(() => this.refresh().subscribe()); - } - /** Restores a persisted session on app bootstrap. Call once from an APP_INITIALIZER or root component. */ - restoreSession() { - this.session.restore(); - } - login() { - this.lastErrorSignal.set(null); - this.loginPhaseSignal.set('requesting-challenge'); - return this.api.requestChallenge().pipe(switchMap(challenge => this.signChallenge(challenge.nonce).pipe(switchMap(({ publicKeyBase64, signature }) => { - this.loginPhaseSignal.set('verifying'); - return this.api.verifySignature({ publicKey: publicKeyBase64, signature, nonce: challenge.nonce }); - }))), tap(tokens => { - this.session.activate(tokens); - this.loginPhaseSignal.set('done'); - }), catchError(error => this.handleAuthError(error, 'invalid-signature'))); - } - refresh() { - const refreshToken = this.session.getRefreshToken(); - if (!refreshToken) { - this.session.markExpired(); - return throwError(() => this.toAuthError({ code: 'session-expired', message: 'No refresh token available.' })); - } - return this.api.refresh({ refreshToken }).pipe(tap(tokens => this.session.activate(tokens)), catchError(error => this.handleAuthError(error, 'session-expired', () => this.session.markExpired()))); - } - logout() { - const refreshToken = this.session.getRefreshToken(); - this.session.clear(); - if (!refreshToken) { - return new Observable(subscriber => { - subscriber.next(); - subscriber.complete(); - }); - } - return this.api.logout(refreshToken).pipe(catchError(() => throwError(() => null))); - } - signChallenge(nonce) { - this.loginPhaseSignal.set('signing'); - return new Observable(subscriber => { - this.keypair - .getOrCreateKeyPair() - .then(({ publicKeyBase64 }) => this.keypair.sign(nonce).then(signature => { - subscriber.next({ publicKeyBase64, signature }); - subscriber.complete(); - })) - .catch(error => subscriber.error(error)); - }); - } - handleAuthError(error, fallbackCode, onError) { - onError?.(); - return throwError(() => this.toAuthError(this.toAuthErrorShape(error, fallbackCode))); - } - toAuthErrorShape(error, fallbackCode) { - if (error instanceof HttpErrorResponse) { - const bodyCode = error.error?.error?.code; - const code = authErrorCodeFromBackendCode(bodyCode) ?? authErrorCodeFromStatus(error.status); - return { code, message: error.message, status: error.status }; - } - if (error instanceof Error) { - return { code: fallbackCode, message: error.message }; - } - return { code: fallbackCode, message: 'Unknown authentication error.' }; - } - toAuthError(error) { - this.lastErrorSignal.set(error); - return error; - } -}; -AuthService = __decorate([ - Injectable({ providedIn: 'root' }) -], AuthService); -export { AuthService }; diff --git a/dist/ed25519/ed25519-keypair.service.d.ts b/dist/ed25519/ed25519-keypair.service.d.ts deleted file mode 100644 index 1673355..0000000 --- a/dist/ed25519/ed25519-keypair.service.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -export declare class Ed25519KeypairService { - private cached; - isSupported(): boolean; - /** Returns the device's Ed25519 keypair, generating and persisting one on first use. */ - getOrCreateKeyPair(): Promise<{ - publicKeyBase64: string; - }>; - sign(message: string): Promise; - /** Discards the local keypair (e.g. "forget this device"). A new keypair on next login requires re-registration with the backend. */ - clear(): Promise; - private generateAndPersist; - private loadFromStore; - private openDatabase; - private toBase64; -} diff --git a/dist/ed25519/ed25519-keypair.service.js b/dist/ed25519/ed25519-keypair.service.js deleted file mode 100644 index 86ff988..0000000 --- a/dist/ed25519/ed25519-keypair.service.js +++ /dev/null @@ -1,114 +0,0 @@ -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -import { Injectable } from '@angular/core'; -/** - * Manages the browser-local Ed25519 keypair used to sign admin auth - * challenges. Real WebCrypto Ed25519 (RFC 8032 support landed in evergreen - * browsers) - not a placeholder. The private key is generated - * non-extractable and kept only in IndexedDB as a CryptoKey handle; it is - * never serialized, never sent anywhere, and cannot be exported by design. - * - * Registering `publicKey` with an admin's account (associating it with a - * role) is a backend-side, out-of-band operation (e.g. an Owner approving a - * new admin's public key) - entirely outside this frontend's scope. - */ -const DB_NAME = 'admin-auth-ed25519'; -const DB_VERSION = 1; -const STORE_NAME = 'keypair'; -const KEY_RECORD_ID = 'device-keypair'; -let Ed25519KeypairService = class Ed25519KeypairService { - constructor() { - this.cached = null; - } - isSupported() { - return typeof crypto !== 'undefined' && !!crypto.subtle && typeof indexedDB !== 'undefined'; - } - /** Returns the device's Ed25519 keypair, generating and persisting one on first use. */ - async getOrCreateKeyPair() { - if (!this.isSupported()) { - throw new Error('Ed25519 is not supported in this browser (requires WebCrypto + IndexedDB).'); - } - const existing = await this.loadFromStore(); - if (existing) { - this.cached = existing; - return { publicKeyBase64: existing.publicKeyBase64 }; - } - const generated = await this.generateAndPersist(); - this.cached = generated; - return { publicKeyBase64: generated.publicKeyBase64 }; - } - async sign(message) { - const keyPair = this.cached ?? (await this.loadFromStore()); - if (!keyPair) { - throw new Error('No Ed25519 keypair available - call getOrCreateKeyPair() first.'); - } - const signatureBuffer = await crypto.subtle.sign('Ed25519', keyPair.privateKey, new TextEncoder().encode(message)); - return this.toBase64(new Uint8Array(signatureBuffer)); - } - /** Discards the local keypair (e.g. "forget this device"). A new keypair on next login requires re-registration with the backend. */ - async clear() { - this.cached = null; - const db = await this.openDatabase(); - await new Promise((resolve, reject) => { - const tx = db.transaction(STORE_NAME, 'readwrite'); - tx.objectStore(STORE_NAME).delete(KEY_RECORD_ID); - tx.oncomplete = () => resolve(); - tx.onerror = () => reject(tx.error); - }); - } - async generateAndPersist() { - const keyPair = (await crypto.subtle.generateKey({ name: 'Ed25519' }, false, ['sign', 'verify'])); - const publicKeyRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey); - const publicKeyBase64 = this.toBase64(new Uint8Array(publicKeyRaw)); - const record = { - id: KEY_RECORD_ID, - publicKey: keyPair.publicKey, - privateKey: keyPair.privateKey, - publicKeyBase64 - }; - const db = await this.openDatabase(); - await new Promise((resolve, reject) => { - const tx = db.transaction(STORE_NAME, 'readwrite'); - tx.objectStore(STORE_NAME).put(record); - tx.oncomplete = () => resolve(); - tx.onerror = () => reject(tx.error); - }); - return record; - } - async loadFromStore() { - const db = await this.openDatabase(); - return new Promise((resolve, reject) => { - const tx = db.transaction(STORE_NAME, 'readonly'); - const request = tx.objectStore(STORE_NAME).get(KEY_RECORD_ID); - request.onsuccess = () => resolve(request.result ?? null); - request.onerror = () => reject(request.error); - }); - } - openDatabase() { - return new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, DB_VERSION); - request.onupgradeneeded = () => { - if (!request.result.objectStoreNames.contains(STORE_NAME)) { - request.result.createObjectStore(STORE_NAME, { keyPath: 'id' }); - } - }; - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); - }); - } - toBase64(bytes) { - let binary = ''; - for (const byte of bytes) { - binary += String.fromCharCode(byte); - } - return btoa(binary); - } -}; -Ed25519KeypairService = __decorate([ - Injectable({ providedIn: 'root' }) -], Ed25519KeypairService); -export { Ed25519KeypairService }; diff --git a/dist/ed25519/ed25519-verification.model.d.ts b/dist/ed25519/ed25519-verification.model.d.ts deleted file mode 100644 index 6c1f277..0000000 --- a/dist/ed25519/ed25519-verification.model.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Observable } from 'rxjs'; -/** - * Prep interfaces for a future Ed25519 challenge/response admin auth flow. - * No crypto is implemented here - verification is delegated to an injectable - * service so the real implementation (native WebCrypto Ed25519 support, or a - * backend verification call) can be swapped in once the backend API exists, - * without touching AdminAuthService or components. - */ -export interface Ed25519Challenge { - nonce: string; - timestamp: string; - /** Opaque challenge payload the client must sign with its private key. */ - payload: string; -} -export interface Ed25519SignedResponse { - challenge: Ed25519Challenge; - publicKey: string; - signature: string; -} -export interface Ed25519VerificationResult { - valid: boolean; - reason?: string; -} -export declare abstract class Ed25519VerificationService { - abstract requestChallenge(): Observable; - abstract verify(response: Ed25519SignedResponse): Observable; -} diff --git a/dist/ed25519/ed25519-verification.model.js b/dist/ed25519/ed25519-verification.model.js deleted file mode 100644 index ba228af..0000000 --- a/dist/ed25519/ed25519-verification.model.js +++ /dev/null @@ -1,2 +0,0 @@ -export class Ed25519VerificationService { -} diff --git a/dist/ed25519/jwt.service.d.ts b/dist/ed25519/jwt.service.d.ts deleted file mode 100644 index 29a5707..0000000 --- a/dist/ed25519/jwt.service.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { JwtClaims } from './models/auth-api.model'; -/** - * Client-side JWT *decoding* only - never verification. The signature is - * meaningless to check here because the frontend has no trusted key to check - * it against; verifying a JWT's signature is the backend's job on every - * request. This service exists purely so the UI can read `role`/`exp` for - * display and route-gating UX (e.g. "session expires in 4m"). - */ -export declare class JwtService { - decode(token: string): JwtClaims | null; - isExpired(claims: JwtClaims, skewSeconds?: number): boolean; - private isJwtClaims; - private base64UrlDecode; -} diff --git a/dist/ed25519/jwt.service.js b/dist/ed25519/jwt.service.js deleted file mode 100644 index 841eb61..0000000 --- a/dist/ed25519/jwt.service.js +++ /dev/null @@ -1,48 +0,0 @@ -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -import { Injectable } from '@angular/core'; -/** - * Client-side JWT *decoding* only - never verification. The signature is - * meaningless to check here because the frontend has no trusted key to check - * it against; verifying a JWT's signature is the backend's job on every - * request. This service exists purely so the UI can read `role`/`exp` for - * display and route-gating UX (e.g. "session expires in 4m"). - */ -let JwtService = class JwtService { - decode(token) { - const parts = token.split('.'); - if (parts.length !== 3) { - return null; - } - try { - const payload = this.base64UrlDecode(parts[1]); - const claims = JSON.parse(payload); - return this.isJwtClaims(claims) ? claims : null; - } - catch { - return null; - } - } - isExpired(claims, skewSeconds = 0) { - return claims.exp * 1000 <= Date.now() + skewSeconds * 1000; - } - isJwtClaims(value) { - if (!value || typeof value !== 'object') { - return false; - } - const claims = value; - return typeof claims.sub === 'string' && typeof claims.role === 'string' && typeof claims.exp === 'number'; - } - base64UrlDecode(input) { - const base64 = input.replace(/-/g, '+').replace(/_/g, '/').padEnd(input.length + ((4 - (input.length % 4)) % 4), '='); - return decodeURIComponent(escape(atob(base64))); - } -}; -JwtService = __decorate([ - Injectable({ providedIn: 'root' }) -], JwtService); -export { JwtService }; diff --git a/dist/ed25519/models/auth-api.model.d.ts b/dist/ed25519/models/auth-api.model.d.ts deleted file mode 100644 index 50623f9..0000000 --- a/dist/ed25519/models/auth-api.model.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { AdminRole } from './permission.model'; -/** Wire contracts for the Ed25519 challenge/response admin auth flow. */ -export interface AuthChallenge { - nonce: string; - /** ISO 8601 issue time of the challenge. */ - issuedAt: string; - /** ISO 8601 - challenge must be used before this or the backend rejects it. */ - expiresAt: string; -} -export interface VerifySignatureRequest { - publicKey: string; - signature: string; - nonce: string; -} -export interface AuthTokenPair { - token: string; - refreshToken: string; -} -export interface RefreshTokenRequest { - refreshToken: string; -} -/** - * Claims expected in the JWT `token`. Decoded client-side for display/UX - * only (role-gating UI, expiry countdown) - the frontend never treats this - * as proof of authorization; every admin request is still re-checked - * server-side. - */ -export interface JwtClaims { - sub: string; - role: AdminRole; - /** Issued-at, seconds since epoch (standard `iat` claim). */ - iat: number; - /** Expiry, seconds since epoch (standard `exp` claim). */ - exp: number; - publicKey: string; -} diff --git a/dist/ed25519/models/auth-api.model.js b/dist/ed25519/models/auth-api.model.js deleted file mode 100644 index cb0ff5c..0000000 --- a/dist/ed25519/models/auth-api.model.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/dist/ed25519/models/auth-error.model.d.ts b/dist/ed25519/models/auth-error.model.d.ts deleted file mode 100644 index b1c2cdc..0000000 --- a/dist/ed25519/models/auth-error.model.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Error codes the Ed25519 admin auth flow can surface to the UI. Each maps to - * a dedicated screen rather than a generic toast, because the recovery - * action differs per code (re-login vs. retry vs. wait). - */ -export type AuthErrorCode = 'session-expired' | 'invalid-signature' | 'unauthorized' | 'forbidden' | 'backend-unavailable'; -export interface AuthError { - code: AuthErrorCode; - message: string; - /** HTTP status that produced this error, when known (absent for client-side errors, e.g. no Ed25519 support). */ - status?: number; -} -export declare function authErrorCodeFromBackendCode(code: unknown): AuthErrorCode | undefined; -/** Maps a backend HTTP status to the AuthErrorCode screen it should route to. */ -export declare function authErrorCodeFromStatus(status: number): AuthErrorCode; diff --git a/dist/ed25519/models/auth-error.model.js b/dist/ed25519/models/auth-error.model.js deleted file mode 100644 index 3c0e348..0000000 --- a/dist/ed25519/models/auth-error.model.js +++ /dev/null @@ -1,24 +0,0 @@ -/** Maps a backend error envelope's `error.code` to the client's AuthErrorCode screens. Only codes with a dedicated screen are mapped; anything else falls back to the HTTP-status-derived code via authErrorCodeFromStatus. */ -const BACKEND_ERROR_CODE_MAP = { - TOKEN_EXPIRED: 'session-expired', - INVALID_SIGNATURE: 'invalid-signature', - UNAUTHENTICATED: 'unauthorized', - FORBIDDEN: 'forbidden', - SERVICE_UNAVAILABLE: 'backend-unavailable', -}; -export function authErrorCodeFromBackendCode(code) { - return typeof code === 'string' ? BACKEND_ERROR_CODE_MAP[code] : undefined; -} -/** Maps a backend HTTP status to the AuthErrorCode screen it should route to. */ -export function authErrorCodeFromStatus(status) { - switch (status) { - case 401: - return 'unauthorized'; - case 403: - return 'forbidden'; - case 0: - return 'backend-unavailable'; - default: - return status >= 500 ? 'backend-unavailable' : 'unauthorized'; - } -} diff --git a/dist/ed25519/models/permission.model.d.ts b/dist/ed25519/models/permission.model.d.ts deleted file mode 100644 index c36fbb4..0000000 --- a/dist/ed25519/models/permission.model.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** Roles the Ed25519 JWT `role` claim is expected to carry. Ordered highest-to-lowest privilege; PermissionService does not rely on the order, it is documentation only. */ -export type AdminRole = 'Owner' | 'Administrator' | 'Editor' | 'Support' | 'ReadOnly'; -/** - * Coarse-grained permission keys. Intentionally small and domain-agnostic - - * fine-grained, per-domain permissions stay server-side; the frontend only - * needs enough to hide/disable UI, never to be the source of truth for - * authorization. - */ -export type Permission = 'backoffice.read' | 'backoffice.write' | 'builder.read' | 'builder.write' | 'users.manage' | 'settings.manage'; -export declare const ROLE_PERMISSIONS: Readonly>; diff --git a/dist/ed25519/models/permission.model.js b/dist/ed25519/models/permission.model.js deleted file mode 100644 index b111921..0000000 --- a/dist/ed25519/models/permission.model.js +++ /dev/null @@ -1,7 +0,0 @@ -export const ROLE_PERMISSIONS = { - Owner: ['backoffice.read', 'backoffice.write', 'builder.read', 'builder.write', 'users.manage', 'settings.manage'], - Administrator: ['backoffice.read', 'backoffice.write', 'builder.read', 'builder.write', 'users.manage'], - Editor: ['backoffice.read', 'backoffice.write', 'builder.read', 'builder.write'], - Support: ['backoffice.read'], - ReadOnly: ['backoffice.read', 'builder.read'] -}; diff --git a/dist/ed25519/noop-ed25519-verification.service.d.ts b/dist/ed25519/noop-ed25519-verification.service.d.ts deleted file mode 100644 index 6a72f14..0000000 --- a/dist/ed25519/noop-ed25519-verification.service.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Observable } from 'rxjs'; -import { Ed25519Challenge, Ed25519SignedResponse, Ed25519VerificationResult, Ed25519VerificationService } from './ed25519-verification.model'; -/** - * Default DI binding for Ed25519VerificationService until the backend ships - * the real challenge/verify endpoints. Intentionally fails closed (throws) - * rather than pretending to verify anything, so accidental use in a login - * path is loud instead of silently accepting unsigned sessions. - */ -export declare class NoopEd25519VerificationService implements Ed25519VerificationService { - requestChallenge(): Observable; - verify(_response: Ed25519SignedResponse): Observable; -} diff --git a/dist/ed25519/noop-ed25519-verification.service.js b/dist/ed25519/noop-ed25519-verification.service.js deleted file mode 100644 index 94a37b7..0000000 --- a/dist/ed25519/noop-ed25519-verification.service.js +++ /dev/null @@ -1,26 +0,0 @@ -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -import { Injectable } from '@angular/core'; -import { throwError } from 'rxjs'; -/** - * Default DI binding for Ed25519VerificationService until the backend ships - * the real challenge/verify endpoints. Intentionally fails closed (throws) - * rather than pretending to verify anything, so accidental use in a login - * path is loud instead of silently accepting unsigned sessions. - */ -let NoopEd25519VerificationService = class NoopEd25519VerificationService { - requestChallenge() { - return throwError(() => new Error('Ed25519 challenge endpoint is not yet available from the backend.')); - } - verify(_response) { - return throwError(() => new Error('Ed25519 verification endpoint is not yet available from the backend.')); - } -}; -NoopEd25519VerificationService = __decorate([ - Injectable({ providedIn: 'root' }) -], NoopEd25519VerificationService); -export { NoopEd25519VerificationService }; diff --git a/dist/ed25519/permission.service.d.ts b/dist/ed25519/permission.service.d.ts deleted file mode 100644 index dae0db0..0000000 --- a/dist/ed25519/permission.service.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Permission } from './models/permission.model'; -/** - * Derives the current admin's permission set from their JWT `role` claim. - * UI-only gate (hide/disable) - the backend must independently enforce - * every mutation server-side. - */ -export declare class PermissionService { - private readonly session; - readonly permissions: import("@angular/core").Signal; - has(permission: Permission): boolean; - hasAny(permissions: readonly Permission[]): boolean; -} diff --git a/dist/ed25519/permission.service.js b/dist/ed25519/permission.service.js deleted file mode 100644 index 0974177..0000000 --- a/dist/ed25519/permission.service.js +++ /dev/null @@ -1,33 +0,0 @@ -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -import { Injectable, computed, inject } from '@angular/core'; -import { ROLE_PERMISSIONS } from './models/permission.model'; -import { SessionService } from './session.service'; -/** - * Derives the current admin's permission set from their JWT `role` claim. - * UI-only gate (hide/disable) - the backend must independently enforce - * every mutation server-side. - */ -let PermissionService = class PermissionService { - constructor() { - this.session = inject(SessionService); - this.permissions = computed(() => { - const role = this.session.role(); - return role ? ROLE_PERMISSIONS[role] : []; - }); - } - has(permission) { - return this.permissions().includes(permission); - } - hasAny(permissions) { - return permissions.some(permission => this.has(permission)); - } -}; -PermissionService = __decorate([ - Injectable({ providedIn: 'root' }) -], PermissionService); -export { PermissionService }; diff --git a/dist/ed25519/session.service.d.ts b/dist/ed25519/session.service.d.ts deleted file mode 100644 index 5580573..0000000 --- a/dist/ed25519/session.service.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { AuthTokenPair, JwtClaims } from './models/auth-api.model'; -export type SessionStatus = 'unknown' | 'restoring' | 'authenticated' | 'unauthenticated' | 'expired'; -/** - * Holds the Ed25519-flow JWT/refresh-token pair and derived claims. Separate - * from the telegram module's AdminAuthService (Telegram-session state) by - * design - the two auth mechanisms are not merged until both ship on the - * same backend and a migration decision is made. - */ -export declare class SessionService { - private readonly jwt; - private readonly tokenSignal; - private readonly refreshTokenSignal; - private readonly claimsSignal; - private readonly statusSignal; - readonly token: import("@angular/core").Signal; - readonly claims: import("@angular/core").Signal; - readonly status: import("@angular/core").Signal; - readonly isAuthenticated: import("@angular/core").Signal; - readonly role: import("@angular/core").Signal; - private refreshTimer?; - private refreshCallback?; - /** Called once by AuthService on init to wire up the refresh trigger without a circular DI dependency. */ - onRefreshDue(callback: () => void): void; - /** Restores session state from persisted storage. Returns true if a (possibly expired) session was found. */ - restore(): boolean; - activate(tokens: AuthTokenPair): void; - getRefreshToken(): string | null; - markExpired(): void; - clear(): void; - private scheduleRefresh; - private clearRefreshTimer; - private readStorage; - private writeStorage; - private removeStorage; -} diff --git a/dist/ed25519/session.service.js b/dist/ed25519/session.service.js deleted file mode 100644 index 8dc8723..0000000 --- a/dist/ed25519/session.service.js +++ /dev/null @@ -1,120 +0,0 @@ -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -import { Injectable, computed, signal } from '@angular/core'; -import { JwtService } from './jwt.service'; -const TOKEN_STORAGE_KEY = 'ed25519AdminToken'; -const REFRESH_STORAGE_KEY = 'ed25519AdminRefreshToken'; -/** Refresh this long before actual expiry, so a request never races an expiring token. */ -const REFRESH_SKEW_MS = 60_000; -/** - * Holds the Ed25519-flow JWT/refresh-token pair and derived claims. Separate - * from the telegram module's AdminAuthService (Telegram-session state) by - * design - the two auth mechanisms are not merged until both ship on the - * same backend and a migration decision is made. - */ -let SessionService = class SessionService { - constructor() { - this.jwt = new JwtService(); - this.tokenSignal = signal(null); - this.refreshTokenSignal = signal(null); - this.claimsSignal = signal(null); - this.statusSignal = signal('unknown'); - this.token = this.tokenSignal.asReadonly(); - this.claims = this.claimsSignal.asReadonly(); - this.status = this.statusSignal.asReadonly(); - this.isAuthenticated = computed(() => this.statusSignal() === 'authenticated'); - this.role = computed(() => this.claimsSignal()?.role ?? null); - } - /** Called once by AuthService on init to wire up the refresh trigger without a circular DI dependency. */ - onRefreshDue(callback) { - this.refreshCallback = callback; - } - /** Restores session state from persisted storage. Returns true if a (possibly expired) session was found. */ - restore() { - this.statusSignal.set('restoring'); - const token = this.readStorage(TOKEN_STORAGE_KEY); - const refreshToken = this.readStorage(REFRESH_STORAGE_KEY); - if (!token || !refreshToken) { - this.statusSignal.set('unauthenticated'); - return false; - } - const claims = this.jwt.decode(token); - if (!claims) { - this.clear(); - return false; - } - this.tokenSignal.set(token); - this.refreshTokenSignal.set(refreshToken); - this.claimsSignal.set(claims); - if (this.jwt.isExpired(claims)) { - this.statusSignal.set('expired'); - } - else { - this.statusSignal.set('authenticated'); - this.scheduleRefresh(claims); - } - return true; - } - activate(tokens) { - const claims = this.jwt.decode(tokens.token); - if (!claims) { - throw new Error('Received a malformed JWT from the auth backend.'); - } - this.tokenSignal.set(tokens.token); - this.refreshTokenSignal.set(tokens.refreshToken); - this.claimsSignal.set(claims); - this.statusSignal.set('authenticated'); - this.writeStorage(TOKEN_STORAGE_KEY, tokens.token); - this.writeStorage(REFRESH_STORAGE_KEY, tokens.refreshToken); - this.scheduleRefresh(claims); - } - getRefreshToken() { - return this.refreshTokenSignal(); - } - markExpired() { - this.statusSignal.set('expired'); - this.clearRefreshTimer(); - } - clear() { - this.tokenSignal.set(null); - this.refreshTokenSignal.set(null); - this.claimsSignal.set(null); - this.statusSignal.set('unauthenticated'); - this.removeStorage(TOKEN_STORAGE_KEY); - this.removeStorage(REFRESH_STORAGE_KEY); - this.clearRefreshTimer(); - } - scheduleRefresh(claims) { - this.clearRefreshTimer(); - const expiresInMs = claims.exp * 1000 - Date.now(); - const refreshInMs = Math.max(expiresInMs - REFRESH_SKEW_MS, 5_000); - this.refreshTimer = setTimeout(() => this.refreshCallback?.(), refreshInMs); - } - clearRefreshTimer() { - if (this.refreshTimer) { - clearTimeout(this.refreshTimer); - this.refreshTimer = undefined; - } - } - readStorage(key) { - return typeof localStorage === 'undefined' ? null : localStorage.getItem(key); - } - writeStorage(key, value) { - if (typeof localStorage !== 'undefined') { - localStorage.setItem(key, value); - } - } - removeStorage(key) { - if (typeof localStorage !== 'undefined') { - localStorage.removeItem(key); - } - } -}; -SessionService = __decorate([ - Injectable({ providedIn: 'root' }) -], SessionService); -export { SessionService }; diff --git a/dist/fesm2022/marketplaces-auth.mjs b/dist/fesm2022/marketplaces-auth.mjs new file mode 100644 index 0000000..88f374d --- /dev/null +++ b/dist/fesm2022/marketplaces-auth.mjs @@ -0,0 +1,1357 @@ +import * as i0 from '@angular/core'; +import { InjectionToken, makeEnvironmentProviders, inject, Injectable, signal, computed, isDevMode, input, booleanAttribute, output, DestroyRef, Component } from '@angular/core'; +import { HttpHeaders, HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { form, required, FormField } from '@angular/forms/signals'; +import * as QRCode from 'qrcode'; +import { map, of, catchError, tap, throwError, timer, switchMap, Observable, finalize } from 'rxjs'; +import { Router } from '@angular/router'; + +/** Base URL for the auth backend, e.g. `https://api.example.com`. Provide from the consuming app's environment config. */ +const AUTH_API_URL = new InjectionToken('@marketplaces/auth AUTH_API_URL'); +/** Telegram bot username used to build QR/deep-link login URLs. Optional — falls back to a default if not provided. */ +const TELEGRAM_BOT_USERNAME = new InjectionToken('@marketplaces/auth TELEGRAM_BOT_USERNAME'); +const MARKETPLACES_AUTH_CONFIG = new InjectionToken('@marketplaces/auth config'); +function provideMarketplacesAuth(config) { + const normalized = { + ...config, + apiUrl: config.apiUrl.replace(/\/$/, ''), + credentialsPath: config.credentialsPath ?? '/auth/credentials/login', + yandexStartPath: config.yandexStartPath ?? '/auth/yandex/sessions', + yandexSessionPath: config.yandexSessionPath ?? '/auth/yandex/sessions', + pollIntervalMs: config.pollIntervalMs ?? 1500, + }; + return makeEnvironmentProviders([ + { provide: MARKETPLACES_AUTH_CONFIG, useValue: normalized }, + { provide: AUTH_API_URL, useValue: normalized.apiUrl }, + ...(normalized.telegramBotUsername + ? [{ provide: TELEGRAM_BOT_USERNAME, useValue: normalized.telegramBotUsername }] + : []), + ]); +} + +const MARKETPLACE_DOMAIN_HEADER = 'X-Marketplace-Domain'; +function normalizeMarketplaceDomain(domain) { + return domain.trim().toLowerCase().replace(/\.$/, ''); +} +class AuthMarketplaceContext { + constructor() { + this.config = inject(MARKETPLACES_AUTH_CONFIG); + } + domain() { + const configured = this.config.marketplaceDomain; + const domain = typeof configured === 'function' + ? configured() + : configured ?? (typeof location === 'undefined' ? '' : location.hostname); + return normalizeMarketplaceDomain(domain); + } + headers(extra) { + const domain = this.domain(); + if (!domain) + throw new Error('Marketplace domain cannot be resolved'); + return new HttpHeaders({ [MARKETPLACE_DOMAIN_HEADER]: domain, ...extra }); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthMarketplaceContext, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } + static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthMarketplaceContext, providedIn: 'root' }); } +} +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthMarketplaceContext, decorators: [{ + type: Injectable, + args: [{ providedIn: 'root' }] + }] }); + +/** RFC4122 v4-ish GUID, using crypto when available. Shared by customer and admin session creation. */ +function generateGuid() { + if (globalThis.crypto?.randomUUID) { + return globalThis.crypto.randomUUID(); + } + const bytes = new Uint8Array(16); + if (globalThis.crypto?.getRandomValues) { + globalThis.crypto.getRandomValues(bytes); + } + else { + for (let index = 0; index < bytes.length; index++) { + bytes[index] = Math.floor(Math.random() * 256); + } + } + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')); + return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`; +} + +const SESSION_MAX_AGE_SECONDS = 60 * 60; +const DEFAULT_TELEGRAM_BOT_USERNAME = 'DexarSupport_bot'; +/** + * The one Telegram QR/session API (`{authApiUrl}/users/sessions`). Customer + * login (AuthService) and admin login (AdminAuthService) both call this same + * service against this same endpoint - there is no separate admin backend. + * This class only does the HTTP call + response normalization; it holds no + * session state and writes no cookies, so each caller manages its own + * storage/signals independently on top of it. + */ +class TelegramSessionApiService { + constructor() { + this.http = inject(HttpClient); + this.authApiUrl = inject(AUTH_API_URL); + this.telegramBotUsername = inject(TELEGRAM_BOT_USERNAME, { optional: true }); + this.marketplaceContext = inject(AuthMarketplaceContext); + } + createSession() { + const webSessionID = generateGuid(); + return this.http.post(`${this.authApiUrl}/users/sessions`, { webSessionID }, { headers: this.marketplaceContext.headers({ WebSessionID: webSessionID }) }).pipe(map(response => { + const responseWebSessionID = this.extractSessionId(response, webSessionID); + return { + webSessionID: responseWebSessionID, + url: this.getBotLoginUrl(responseWebSessionID), + }; + })); + } + checkSessionOnce(webSessionID) { + if (!webSessionID) { + return of(null); + } + return this.http.get(`${this.authApiUrl}/users/sessions/${encodeURIComponent(webSessionID)}`, { headers: this.marketplaceContext.headers() }).pipe(map(response => this.normalizeWebSession(response, webSessionID)), catchError(() => of(null))); + } + logout(webSessionID) { + return this.http.delete(`${this.authApiUrl}/users/sessions/${encodeURIComponent(webSessionID)}`, { + headers: this.marketplaceContext.headers({ WebSessionID: webSessionID }) + }).pipe(catchError(() => of(null))); + } + getBotLoginUrl(webSessionID) { + return `https://t.me/${this.getBotUsername()}?start=${encodeURIComponent(webSessionID)}`; + } + getBotAppLoginUrl(webSessionID) { + return `tg://resolve?domain=${encodeURIComponent(this.getBotUsername())}&start=${encodeURIComponent(webSessionID)}`; + } + getBotUsername() { + return this.telegramBotUsername || DEFAULT_TELEGRAM_BOT_USERNAME; + } + normalizeWebSession(response, fallbackSessionId) { + if (!response) { + return null; + } + const user = this.asRecord(this.readFirst(response, ['user', 'User', 'telegramUser', 'TelegramUser'])) ?? response; + const status = this.readFirst(response, [ + 'status', 'Status', 'active', 'Active', 'loggedIn', 'LoggedIn', + 'isLoggedIn', 'IsLoggedIn', 'authenticated', 'Authenticated' + ]); + const active = this.isActiveStatus(status); + const sessionId = this.extractSessionId(response, fallbackSessionId); + const username = this.readString(this.readFirst(user, ['username', 'Username'])) + ?? this.readString(this.readFirst(response, ['username', 'Username'])); + const firstName = this.readString(this.readFirst(user, ['firstName', 'first_name', 'FirstName', 'First_name'])); + const lastName = this.readString(this.readFirst(user, ['lastName', 'last_name', 'LastName', 'Last_name'])); + const fullName = [firstName, lastName].filter(Boolean).join(' '); + const explicitDisplayName = this.readString(this.readFirst(response, ['displayName', 'DisplayName', 'name', 'Name'])) + ?? this.readString(this.readFirst(user, ['displayName', 'DisplayName', 'name', 'Name'])); + const displayName = explicitDisplayName ?? username ?? (fullName || 'Telegram User'); + const telegramUserId = this.readNumber(this.readFirst(user, ['userId', 'telegramUserId', 'telegramUserID', 'TelegramUserID', 'id', 'ID'])) + ?? this.readNumber(this.readFirst(response, ['userId', 'telegramUserId', 'telegramUserID', 'TelegramUserID', 'userID', 'UserID', 'UserId'])) + ?? null; + const expiresAt = this.readString(this.readFirst(response, ['expiresAt', 'ExpiresAt', 'expires', 'Expires'])) + ?? new Date(Date.now() + SESSION_MAX_AGE_SECONDS * 1000).toISOString(); + return { sessionId, userId: telegramUserId, username, displayName, active, expires: expiresAt }; + } + extractSessionId(response, fallbackSessionId) { + if (!response) { + return fallbackSessionId; + } + return this.readString(this.readFirst(response, [ + 'webSessionID', 'WebSessionID', 'webSessionId', 'sessionID', 'SessionID', 'sessionId', 'id', 'ID' + ])) ?? fallbackSessionId; + } + readFirst(source, keys) { + for (const key of keys) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + return source[key]; + } + } + return undefined; + } + readString(value) { + if (typeof value === 'string' && value.trim()) { + return value; + } + if (typeof value === 'number' || typeof value === 'bigint') { + return value.toString(); + } + return null; + } + readNumber(value) { + if (typeof value === 'number' && Number.isFinite(value)) { + return value; + } + if (typeof value === 'string') { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; + } + return null; + } + asRecord(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value) + ? value + : null; + } + isActiveStatus(status) { + if (status === true || status === 1) { + return true; + } + if (typeof status !== 'string') { + return false; + } + return ['true', '1', 'active', 'authenticated', 'confirmed', 'success', 'logged_in'].includes(status.toLowerCase()); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: TelegramSessionApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } + static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: TelegramSessionApiService, providedIn: 'root' }); } +} +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: TelegramSessionApiService, decorators: [{ + type: Injectable, + args: [{ providedIn: 'root' }] + }] }); + +/** + * Admin login uses the exact same Telegram QR/session API as the customer + * login (TelegramSessionApiService) - there is no separate admin backend + * endpoint, and none should be invented client-side. Only the *storage* is + * kept separate from AuthService, so an admin QR scan never authenticates + * the customer session or vice versa: distinct cookie name, distinct + * signals, distinct guard/interceptor. + * + * Since the session API itself has no concept of "admin", the frontend + * cannot tell an admin Telegram session from a regular one. Actual admin + * authorization must be enforced server-side when admin API calls are made + * with the resulting session id - the frontend only decides where to + * *store* the result. + */ +const ADMIN_SESSION_COOKIE = 'adminSessionID'; +const ADMIN_TOKEN_STORAGE_KEY = 'adminToken'; +const ADMIN_REFRESH_STORAGE_KEY = 'adminRefreshToken'; +const ADMIN_SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60; +class AdminAuthService { + constructor() { + this.api = inject(TelegramSessionApiService); + this.sessionSignal = signal(null, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "sessionSignal" }] : /* istanbul ignore next */ [])); + this.statusSignal = signal('unknown', /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "statusSignal" }] : /* istanbul ignore next */ [])); + this.showLoginSignal = signal(false, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "showLoginSignal" }] : /* istanbul ignore next */ [])); + this.session = this.sessionSignal.asReadonly(); + this.status = this.statusSignal.asReadonly(); + this.isAuthenticated = computed(() => this.statusSignal() === 'authenticated', /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : /* istanbul ignore next */ [])); + this.showLoginDialog = this.showLoginSignal.asReadonly(); + this.displayName = computed(() => this.sessionSignal()?.displayName ?? null, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "displayName" }] : /* istanbul ignore next */ [])); + this.checkSession(); + } + checkSession() { + const webSessionID = this.getStoredAdminSessionID(); + if (!webSessionID) { + this.clearAuthState('unauthenticated'); + return; + } + this.statusSignal.set('checking'); + this.checkSessionOnce(webSessionID).subscribe(session => { + if (!session?.active) { + this.clearAuthState('unauthenticated'); + } + }); + } + /** Check session without mutating internal state beyond activating on success (used for polling). */ + checkSessionOnce(webSessionID = this.getStoredAdminSessionID()) { + return this.api.checkSessionOnce(webSessionID).pipe(tap(session => { + if (session?.active) { + this.activateSession(session); + } + })); + } + /** Create a backend web session - identical call to the customer login (TelegramSessionApiService.createSession). */ + createWebSession() { + return this.api.createSession(); + } + getAdminAppLoginUrl(webSessionID) { + return this.api.getBotAppLoginUrl(webSessionID); + } + onLoginComplete() { + this.hideLogin(); + if (!this.isAuthenticated()) { + this.checkSession(); + } + } + requestLogin() { + this.showLoginSignal.set(true); + } + /** + * Dev-only shortcut for local testing without a reachable Telegram/session + * backend: fabricates a local session and activates it directly, skipping + * the QR flow entirely. No-ops in production builds (checked via Angular's + * isDevMode() at runtime, not just build-time, so it is safe even if this + * code ships). Never call this from anywhere reachable in a production build. + */ + devBypassLogin() { + if (!isDevMode()) { + return; + } + this.hideLogin(); + this.activateSession({ + sessionId: `dev-bypass-${Date.now()}`, + userId: 0, + username: 'dev-admin', + displayName: 'Dev Admin (local bypass)', + active: true, + expires: new Date(Date.now() + 60 * 60 * 1000).toISOString(), + }); + } + hideLogin() { + this.showLoginSignal.set(false); + } + logout() { + const webSessionID = this.sessionSignal()?.sessionId || this.getStoredAdminSessionID(); + if (!webSessionID) { + this.clearAuthState('unauthenticated'); + return; + } + this.api.logout(webSessionID).subscribe(() => this.clearAuthState('unauthenticated')); + } + /** Accept a session/tokens returned by credentials or an external provider. */ + acceptSession(session, token, refreshToken) { + this.activateSession(session); + if (token && refreshToken) + this.setAdminTokens(token, refreshToken); + } + /** JWT pair storage, reserved for once the backend issues admin access/refresh tokens. Unused until then. */ + getAdminToken() { + return typeof localStorage === 'undefined' ? null : localStorage.getItem(ADMIN_TOKEN_STORAGE_KEY); + } + setAdminTokens(token, refreshToken) { + if (typeof localStorage === 'undefined') { + return; + } + localStorage.setItem(ADMIN_TOKEN_STORAGE_KEY, token); + localStorage.setItem(ADMIN_REFRESH_STORAGE_KEY, refreshToken); + } + clearAdminTokens() { + if (typeof localStorage === 'undefined') { + return; + } + localStorage.removeItem(ADMIN_TOKEN_STORAGE_KEY); + localStorage.removeItem(ADMIN_REFRESH_STORAGE_KEY); + } + activateSession(session) { + this.sessionSignal.set(session); + this.statusSignal.set('authenticated'); + this.setStoredAdminSessionID(session.sessionId); + this.scheduleSessionRefresh(session.expires); + } + clearAuthState(status) { + this.sessionSignal.set(null); + this.statusSignal.set(status); + this.clearStoredAdminSessionID(); + this.clearAdminTokens(); + this.clearSessionRefresh(); + } + scheduleSessionRefresh(expiresAt) { + this.clearSessionRefresh(); + const expiresMs = new Date(expiresAt).getTime(); + const nowMs = Date.now(); + const refreshIn = Number.isFinite(expiresMs) + ? Math.max(expiresMs - nowMs - 60_000, 30_000) + : ADMIN_SESSION_COOKIE_MAX_AGE_SECONDS * 1000; + this.sessionCheckTimer = setTimeout(() => this.checkSession(), refreshIn); + } + clearSessionRefresh() { + if (this.sessionCheckTimer) { + clearTimeout(this.sessionCheckTimer); + this.sessionCheckTimer = undefined; + } + } + getStoredAdminSessionID() { + if (typeof document === 'undefined') { + return null; + } + const cookie = document.cookie.split('; ').find(row => row.startsWith(`${ADMIN_SESSION_COOKIE}=`)); + if (!cookie) { + return null; + } + try { + return decodeURIComponent(cookie.substring(ADMIN_SESSION_COOKIE.length + 1)); + } + catch { + return null; + } + } + setStoredAdminSessionID(webSessionID) { + if (typeof document === 'undefined') { + return; + } + const secure = typeof window !== 'undefined' && window.location.protocol === 'https:' ? '; Secure' : ''; + document.cookie = `${ADMIN_SESSION_COOKIE}=${encodeURIComponent(webSessionID)}; Max-Age=${ADMIN_SESSION_COOKIE_MAX_AGE_SECONDS}; Path=/; SameSite=Strict${secure}`; + } + clearStoredAdminSessionID() { + if (typeof document === 'undefined') { + return; + } + document.cookie = `${ADMIN_SESSION_COOKIE}=; Max-Age=0; Path=/; SameSite=Strict`; + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AdminAuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } + static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AdminAuthService, providedIn: 'root' }); } +} +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AdminAuthService, decorators: [{ + type: Injectable, + args: [{ providedIn: 'root' }] + }], ctorParameters: () => [] }); + +const WEB_SESSION_COOKIE = 'webSessionID'; +const WEB_SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60; +/** Customer-facing Telegram QR/session auth. Distinct storage/state from AdminAuthService by design. */ +let AuthService$1 = class AuthService { + constructor() { + this.api = inject(TelegramSessionApiService); + this.sessionSignal = signal(null, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "sessionSignal" }] : /* istanbul ignore next */ [])); + this.statusSignal = signal('unknown', /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "statusSignal" }] : /* istanbul ignore next */ [])); + this.showLoginSignal = signal(false, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "showLoginSignal" }] : /* istanbul ignore next */ [])); + /** Current auth session */ + this.session = this.sessionSignal.asReadonly(); + /** Current auth status */ + this.status = this.statusSignal.asReadonly(); + /** Whether user is fully authenticated */ + this.isAuthenticated = computed(() => this.statusSignal() === 'authenticated', /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : /* istanbul ignore next */ [])); + /** Whether to show login dialog */ + this.showLoginDialog = this.showLoginSignal.asReadonly(); + /** Display name of authenticated user */ + this.displayName = computed(() => this.sessionSignal()?.displayName ?? null, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "displayName" }] : /* istanbul ignore next */ [])); + // On init, check existing session via cookie + this.checkSession(); + } + /** Check the current webSessionID cookie against the auth backend. */ + checkSession() { + const webSessionID = this.getStoredWebSessionID(); + if (!webSessionID) { + this.clearAuthState('unauthenticated'); + return; + } + this.statusSignal.set('checking'); + this.checkSessionOnce(webSessionID).subscribe(session => { + if (!session?.active) { + this.clearAuthState('unauthenticated'); + } + }); + } + /** Check session without updating internal state beyond activating on success (used for polling). */ + checkSessionOnce(webSessionID = this.getStoredWebSessionID()) { + return this.api.checkSessionOnce(webSessionID).pipe(tap(session => { + if (session?.active) { + this.activateSession(session); + } + })); + } + /** + * Called after user completes Telegram login. + */ + onTelegramLoginComplete() { + this.hideLogin(); + if (!this.isAuthenticated()) { + this.checkSession(); + } + } + /** Generate the Telegram login URL for bot-based auth */ + getTelegramLoginUrl(webSessionID) { + return this.api.getBotLoginUrl(webSessionID); + } + /** Generate a Telegram app deep link for mobile login without opening a browser tab. */ + getTelegramAppLoginUrl(webSessionID) { + return this.api.getBotAppLoginUrl(webSessionID); + } + /** Create a backend web session and return the Telegram start link for it. */ + createWebSession() { + return this.api.createSession(); + } + /** Show login dialog (called when user tries to pay without being logged in) */ + requestLogin() { + this.showLoginSignal.set(true); + } + /** Hide login dialog */ + hideLogin() { + this.showLoginSignal.set(false); + } + /** Logout — clears session on backend and locally */ + logout() { + const webSessionID = this.sessionSignal()?.sessionId || this.getStoredWebSessionID(); + if (!webSessionID) { + this.clearAuthState('unauthenticated'); + return; + } + this.api.logout(webSessionID).subscribe(() => { + this.clearAuthState('unauthenticated'); + }); + } + /** Accept a session returned by credentials or an external provider. */ + acceptSession(session) { + this.activateSession(session); + } + activateSession(session) { + this.sessionSignal.set(session); + this.statusSignal.set('authenticated'); + this.setStoredWebSessionID(session.sessionId); + this.scheduleSessionRefresh(session.expires); + } + clearAuthState(status) { + this.sessionSignal.set(null); + this.statusSignal.set(status); + this.clearStoredWebSessionID(); + this.clearSessionRefresh(); + } + /** Schedule a session re-check before it expires */ + scheduleSessionRefresh(expiresAt) { + this.clearSessionRefresh(); + const expiresMs = new Date(expiresAt).getTime(); + const nowMs = Date.now(); + // Re-check 60 seconds before expiry, minimum 30s from now + const refreshIn = Number.isFinite(expiresMs) + ? Math.max(expiresMs - nowMs - 60_000, 30_000) + : WEB_SESSION_COOKIE_MAX_AGE_SECONDS * 1000; + this.sessionCheckTimer = setTimeout(() => { + this.checkSession(); + }, refreshIn); + } + clearSessionRefresh() { + if (this.sessionCheckTimer) { + clearTimeout(this.sessionCheckTimer); + this.sessionCheckTimer = undefined; + } + } + getStoredWebSessionID() { + if (typeof document === 'undefined') { + return null; + } + const cookie = document.cookie + .split('; ') + .find(row => row.startsWith(`${WEB_SESSION_COOKIE}=`)); + if (!cookie) { + return null; + } + try { + return decodeURIComponent(cookie.substring(WEB_SESSION_COOKIE.length + 1)); + } + catch { + return null; + } + } + setStoredWebSessionID(webSessionID) { + if (typeof document === 'undefined') { + return; + } + const secure = typeof window !== 'undefined' && window.location.protocol === 'https:' ? '; Secure' : ''; + document.cookie = `${WEB_SESSION_COOKIE}=${encodeURIComponent(webSessionID)}; Max-Age=${WEB_SESSION_COOKIE_MAX_AGE_SECONDS}; Path=/; SameSite=Lax${secure}`; + } + clearStoredWebSessionID() { + if (typeof document === 'undefined') { + return; + } + document.cookie = `${WEB_SESSION_COOKIE}=; Max-Age=0; Path=/; SameSite=Lax`; + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } + static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthService, providedIn: 'root' }); } +}; +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthService$1, decorators: [{ + type: Injectable, + args: [{ + providedIn: 'root' + }] + }], ctorParameters: () => [] }); + +const MARKETPLACES_AUTH_GATEWAY = new InjectionToken('@marketplaces/auth gateway', { providedIn: 'root', factory: () => inject(HttpMarketplacesAuthGateway) }); +class HttpMarketplacesAuthGateway { + constructor() { + this.http = inject(HttpClient); + this.config = inject(MARKETPLACES_AUTH_CONFIG); + this.context = inject(AuthMarketplaceContext); + this.customerAuth = inject(AuthService$1); + this.adminAuth = inject(AdminAuthService); + } + startQr(mode) { + return mode === 'admin' ? this.adminAuth.createWebSession() : this.customerAuth.createWebSession(); + } + checkQr(mode, attemptId) { + return mode === 'admin' ? this.adminAuth.checkSessionOnce(attemptId) : this.customerAuth.checkSessionOnce(attemptId); + } + loginWithCredentials(mode, credentials) { + return this.http.post(this.url(this.config.credentialsPath), { ...credentials, mode }, { + headers: this.context.headers(), + }).pipe(map(result => this.accept(mode, { ...result, method: 'credentials', mode })), catchError(error => throwError(() => this.failure('credentials', error)))); + } + startYandex(mode, returnUrl) { + return this.http.post(this.url(this.config.yandexStartPath), { + provider: 'yandex', mode, returnUrl, + }, { headers: this.context.headers() }).pipe(catchError(error => throwError(() => this.failure('yandex', error)))); + } + checkYandex(mode, attemptId) { + return this.http.get(`${this.url(this.config.yandexSessionPath)}/${encodeURIComponent(attemptId)}`, { headers: this.context.headers() }).pipe(map(result => result ? this.accept(mode, { ...result, method: 'yandex', mode }) : null), catchError((error) => error.status === 404 || error.status === 202 + ? of(null) + : throwError(() => this.failure('yandex', error)))); + } + accept(mode, result) { + if (mode === 'admin') + this.adminAuth.acceptSession(result.session, result.accessToken, result.refreshToken); + else + this.customerAuth.acceptSession(result.session); + return result; + } + url(path = '') { return `${this.config.apiUrl}${path.startsWith('/') ? path : `/${path}`}`; } + failure(method, cause) { + const response = cause instanceof HttpErrorResponse ? cause : null; + return { + method, + code: response?.status === 401 ? 'invalid_credentials' : 'backend', + message: response?.error?.message || response?.message || 'Authentication failed', + cause, + }; + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HttpMarketplacesAuthGateway, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } + static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HttpMarketplacesAuthGateway, providedIn: 'root' }); } +} +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HttpMarketplacesAuthGateway, decorators: [{ + type: Injectable, + args: [{ providedIn: 'root' }] + }] }); + +class MarketplacesAuthComponent { + constructor() { + this.qr = input(false, { ...(ngDevMode ? { debugName: "qr" } : /* istanbul ignore next */ {}), transform: booleanAttribute }); + this.credentials = input(false, { ...(ngDevMode ? { debugName: "credentials" } : /* istanbul ignore next */ {}), transform: booleanAttribute }); + this.yandex = input(false, { ...(ngDevMode ? { debugName: "yandex" } : /* istanbul ignore next */ {}), transform: booleanAttribute }); + this.mode = input('customer', /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ [])); + this.title = input('Вход', /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ [])); + this.authenticated = output(); + this.authError = output(); + this.cancelled = output(); + this.method = signal(null, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "method" }] : /* istanbul ignore next */ [])); + this.busy = signal(false, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ [])); + this.error = signal(null, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ [])); + this.qrImage = signal(null, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "qrImage" }] : /* istanbul ignore next */ [])); + this.externalUrl = signal(null, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "externalUrl" }] : /* istanbul ignore next */ [])); + this.credentialsModel = signal({ login: '', password: '' }, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "credentialsModel" }] : /* istanbul ignore next */ [])); + this.credentialsForm = form(this.credentialsModel, path => { + required(path.login, { message: 'Введите логин' }); + required(path.password, { message: 'Введите пароль' }); + }); + this.gateway = inject(MARKETPLACES_AUTH_GATEWAY); + this.config = inject(MARKETPLACES_AUTH_CONFIG); + inject(DestroyRef).onDestroy(() => this.poll?.unsubscribe()); + queueMicrotask(() => this.select(this.qr() ? 'qr' : this.credentials() ? 'credentials' : this.yandex() ? 'yandex' : null)); + } + select(method) { this.poll?.unsubscribe(); this.busy.set(false); this.error.set(null); this.method.set(method); } + startQr() { + this.begin(); + this.gateway.startQr(this.mode()).subscribe({ + next: attempt => void this.prepareQr(attempt.url, attempt.webSessionID).catch(cause => this.fail('qr', cause)), + error: cause => this.fail('qr', cause), + }); + } + loginWithCredentials(event) { + event.preventDefault(); + if (this.credentialsForm().invalid()) + return; + this.begin(); + this.gateway.loginWithCredentials(this.mode(), this.credentialsModel()).subscribe({ + next: result => this.finish(result), error: cause => this.fail('credentials', cause), + }); + } + startYandex() { + this.begin(); + const returnUrl = typeof location === 'undefined' ? '' : location.href; + this.gateway.startYandex(this.mode(), returnUrl).subscribe({ + next: attempt => { + const popup = typeof window === 'undefined' ? null : window.open(attempt.authorizationUrl, 'mp-yandex-auth', 'popup,width=520,height=720'); + if (!popup) { + this.fail('yandex', { method: 'yandex', code: 'popup_blocked', message: 'Браузер заблокировал окно Яндекса' }); + return; + } + this.pollForYandex(attempt.attemptId); + }, + error: cause => this.fail('yandex', cause), + }); + } + pollForQr(attemptId) { + this.poll?.unsubscribe(); + this.poll = timer(0, this.config.pollIntervalMs ?? 1500).pipe(switchMap(() => this.gateway.checkQr(this.mode(), attemptId))) + .subscribe({ next: session => { if (session?.active) + this.finish({ method: 'qr', mode: this.mode(), session }); }, error: cause => this.fail('qr', cause) }); + } + async prepareQr(url, attemptId) { + this.externalUrl.set(url); + this.qrImage.set(await QRCode.toDataURL(url, { width: 320, margin: 1 })); + this.pollForQr(attemptId); + } + pollForYandex(attemptId) { + this.poll?.unsubscribe(); + this.poll = timer(0, this.config.pollIntervalMs ?? 1500).pipe(switchMap(() => this.gateway.checkYandex(this.mode(), attemptId))) + .subscribe({ next: result => { if (result) + this.finish(result); }, error: cause => this.fail('yandex', cause) }); + } + begin() { this.poll?.unsubscribe(); this.error.set(null); this.busy.set(true); } + finish(result) { this.poll?.unsubscribe(); this.busy.set(false); this.authenticated.emit(result); } + fail(method, cause) { + this.poll?.unsubscribe(); + this.busy.set(false); + const failure = this.isFailure(cause) ? cause : { method, code: 'backend', message: 'Не удалось выполнить вход', cause }; + this.error.set(failure); + this.authError.emit(failure); + } + isFailure(value) { return !!value && typeof value === 'object' && 'code' in value && 'message' in value; } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: MarketplacesAuthComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } + static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: MarketplacesAuthComponent, isStandalone: true, selector: "mp-auth, marketplaces-auth", inputs: { qr: { classPropertyName: "qr", publicName: "qr", isSignal: true, isRequired: false, transformFunction: null }, credentials: { classPropertyName: "credentials", publicName: "credentials", isSignal: true, isRequired: false, transformFunction: null }, yandex: { classPropertyName: "yandex", publicName: "yandex", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { authenticated: "authenticated", authError: "authError", cancelled: "cancelled" }, ngImport: i0, template: ` +
+

{{ title() }}

+
+ @if (qr()) { } + @if (credentials()) { } + @if (yandex()) { } +
+ @if (method() === 'credentials') { +
+ + + +
+ } + @if (method() === 'qr') { + @if (qrImage()) { QR-код для входа } + + } + @if (method() === 'yandex') { } + @if (busy()) {

Ожидаем подтверждение…

} + @if (error()) { } +
+ `, isInline: true, styles: [":host{display:block}.mp-auth{font:inherit;color:inherit;display:grid;gap:1rem;max-width:25rem}h2,p{margin:0}.methods{display:flex;gap:.5rem;flex-wrap:wrap}.methods button{background:transparent;color:inherit}button,input{font:inherit;border:1px solid #c7c7c7;border-radius:.65rem;padding:.7rem .9rem}button{cursor:pointer}.active,button[type=submit]{background:#111;color:#fff;border-color:#111}button:disabled{opacity:.55;cursor:wait}form{display:grid;gap:.8rem}label{display:grid;gap:.35rem}img{display:block;width:min(15rem,100%);height:auto;border-radius:.75rem}.error{color:#b42318}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"], exportAs: ["formField"] }] }); } +} +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: MarketplacesAuthComponent, decorators: [{ + type: Component, + args: [{ selector: 'mp-auth, marketplaces-auth', standalone: true, imports: [FormField], template: ` +
+

{{ title() }}

+
+ @if (qr()) { } + @if (credentials()) { } + @if (yandex()) { } +
+ @if (method() === 'credentials') { +
+ + + +
+ } + @if (method() === 'qr') { + @if (qrImage()) { QR-код для входа } + + } + @if (method() === 'yandex') { } + @if (busy()) {

Ожидаем подтверждение…

} + @if (error()) { } +
+ `, styles: [":host{display:block}.mp-auth{font:inherit;color:inherit;display:grid;gap:1rem;max-width:25rem}h2,p{margin:0}.methods{display:flex;gap:.5rem;flex-wrap:wrap}.methods button{background:transparent;color:inherit}button,input{font:inherit;border:1px solid #c7c7c7;border-radius:.65rem;padding:.7rem .9rem}button{cursor:pointer}.active,button[type=submit]{background:#111;color:#fff;border-color:#111}button:disabled{opacity:.55;cursor:wait}form{display:grid;gap:.8rem}label{display:grid;gap:.35rem}img{display:block;width:min(15rem,100%);height:auto;border-radius:.75rem}.error{color:#b42318}\n"] }] + }], ctorParameters: () => [], propDecorators: { qr: [{ type: i0.Input, args: [{ isSignal: true, alias: "qr", required: false }] }], credentials: [{ type: i0.Input, args: [{ isSignal: true, alias: "credentials", required: false }] }], yandex: [{ type: i0.Input, args: [{ isSignal: true, alias: "yandex", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], authenticated: [{ type: i0.Output, args: ["authenticated"] }], authError: [{ type: i0.Output, args: ["authError"] }], cancelled: [{ type: i0.Output, args: ["cancelled"] }] } }); + +/** Guards `/admin/**`-style routes. Never shares state with the customer auth guard/service. */ +const adminAuthGuard = () => { + const adminAuth = inject(AdminAuthService); + if (adminAuth.isAuthenticated()) { + return true; + } + adminAuth.requestLogin(); + return false; +}; + +/** Backend paths that require an active AdminWebSessionID. Adjust to match your API surface if consuming this outside marketplaces. */ +const ADMIN_GATED_PATH_SEGMENTS = ['/admin/', '/backoffice/', '/builder/', '/media/']; +/** + * Attaches admin session/token headers only to admin API requests. Scoped to + * admin-gated paths so it never touches customer requests and never reads + * the customer AuthService's session. + */ +const adminAuthHeadersInterceptor = (req, next) => { + const isAdminRequest = ADMIN_GATED_PATH_SEGMENTS.some(segment => req.url.includes(segment)); + if (!isAdminRequest) { + return next(req); + } + const adminAuth = inject(AdminAuthService); + const session = adminAuth.session(); + const token = adminAuth.getAdminToken(); + let headers = req.headers; + if (session?.sessionId) { + headers = headers.set('AdminWebSessionID', session.sessionId); + } + if (token) { + headers = headers.set('Authorization', `Bearer ${token}`); + } + return next(req.clone({ headers })); +}; + +/** Maps a backend error envelope's `error.code` to the client's AuthErrorCode screens. Only codes with a dedicated screen are mapped; anything else falls back to the HTTP-status-derived code via authErrorCodeFromStatus. */ +const BACKEND_ERROR_CODE_MAP = { + TOKEN_EXPIRED: 'session-expired', + INVALID_SIGNATURE: 'invalid-signature', + UNAUTHENTICATED: 'unauthorized', + FORBIDDEN: 'forbidden', + SERVICE_UNAVAILABLE: 'backend-unavailable', +}; +function authErrorCodeFromBackendCode(code) { + return typeof code === 'string' ? BACKEND_ERROR_CODE_MAP[code] : undefined; +} +/** Maps a backend HTTP status to the AuthErrorCode screen it should route to. */ +function authErrorCodeFromStatus(status) { + switch (status) { + case 401: + return 'unauthorized'; + case 403: + return 'forbidden'; + case 0: + return 'backend-unavailable'; + default: + return status >= 500 ? 'backend-unavailable' : 'unauthorized'; + } +} + +/** + * Thin HTTP client for the Ed25519 admin auth endpoints. These endpoints may + * not exist on every backend yet - calling them before the backend ships + * 404s or connection-errors, which AuthService maps to the + * `backend-unavailable` error screen. No mock/fake responses are fabricated + * here; this is real HttpClient wiring against the real contract. + */ +class AuthApiService { + constructor() { + this.http = inject(HttpClient); + this.baseUrl = `${inject(AUTH_API_URL)}/api/admin/auth`; + } + requestChallenge() { + return this.http.get(`${this.baseUrl}/challenge`); + } + verifySignature(request) { + return this.http.post(`${this.baseUrl}/verify`, request); + } + refresh(request) { + return this.http.post(`${this.baseUrl}/refresh`, request); + } + logout(refreshToken) { + return this.http.post(`${this.baseUrl}/logout`, { refreshToken }); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } + static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthApiService, providedIn: 'root' }); } +} +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthApiService, decorators: [{ + type: Injectable, + args: [{ providedIn: 'root' }] + }] }); + +/** + * Manages the browser-local Ed25519 keypair used to sign admin auth + * challenges. Real WebCrypto Ed25519 (RFC 8032 support landed in evergreen + * browsers) - not a placeholder. The private key is generated + * non-extractable and kept only in IndexedDB as a CryptoKey handle; it is + * never serialized, never sent anywhere, and cannot be exported by design. + * + * Registering `publicKey` with an admin's account (associating it with a + * role) is a backend-side, out-of-band operation (e.g. an Owner approving a + * new admin's public key) - entirely outside this frontend's scope. + */ +const DB_NAME = 'admin-auth-ed25519'; +const DB_VERSION = 1; +const STORE_NAME = 'keypair'; +const KEY_RECORD_ID = 'device-keypair'; +class Ed25519KeypairService { + constructor() { + this.cached = null; + } + isSupported() { + return typeof crypto !== 'undefined' && !!crypto.subtle && typeof indexedDB !== 'undefined'; + } + /** Returns the device's Ed25519 keypair, generating and persisting one on first use. */ + async getOrCreateKeyPair() { + if (!this.isSupported()) { + throw new Error('Ed25519 is not supported in this browser (requires WebCrypto + IndexedDB).'); + } + const existing = await this.loadFromStore(); + if (existing) { + this.cached = existing; + return { publicKeyBase64: existing.publicKeyBase64 }; + } + const generated = await this.generateAndPersist(); + this.cached = generated; + return { publicKeyBase64: generated.publicKeyBase64 }; + } + async sign(message) { + const keyPair = this.cached ?? (await this.loadFromStore()); + if (!keyPair) { + throw new Error('No Ed25519 keypair available - call getOrCreateKeyPair() first.'); + } + const signatureBuffer = await crypto.subtle.sign('Ed25519', keyPair.privateKey, new TextEncoder().encode(message)); + return this.toBase64(new Uint8Array(signatureBuffer)); + } + /** Discards the local keypair (e.g. "forget this device"). A new keypair on next login requires re-registration with the backend. */ + async clear() { + this.cached = null; + const db = await this.openDatabase(); + await new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readwrite'); + tx.objectStore(STORE_NAME).delete(KEY_RECORD_ID); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + } + async generateAndPersist() { + const keyPair = (await crypto.subtle.generateKey({ name: 'Ed25519' }, false, ['sign', 'verify'])); + const publicKeyRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey); + const publicKeyBase64 = this.toBase64(new Uint8Array(publicKeyRaw)); + const record = { + id: KEY_RECORD_ID, + publicKey: keyPair.publicKey, + privateKey: keyPair.privateKey, + publicKeyBase64 + }; + const db = await this.openDatabase(); + await new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readwrite'); + tx.objectStore(STORE_NAME).put(record); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + return record; + } + async loadFromStore() { + const db = await this.openDatabase(); + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readonly'); + const request = tx.objectStore(STORE_NAME).get(KEY_RECORD_ID); + request.onsuccess = () => resolve(request.result ?? null); + request.onerror = () => reject(request.error); + }); + } + openDatabase() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + request.onupgradeneeded = () => { + if (!request.result.objectStoreNames.contains(STORE_NAME)) { + request.result.createObjectStore(STORE_NAME, { keyPath: 'id' }); + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + toBase64(bytes) { + let binary = ''; + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + return btoa(binary); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: Ed25519KeypairService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } + static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: Ed25519KeypairService, providedIn: 'root' }); } +} +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: Ed25519KeypairService, decorators: [{ + type: Injectable, + args: [{ providedIn: 'root' }] + }] }); + +/** + * Client-side JWT *decoding* only - never verification. The signature is + * meaningless to check here because the frontend has no trusted key to check + * it against; verifying a JWT's signature is the backend's job on every + * request. This service exists purely so the UI can read `role`/`exp` for + * display and route-gating UX (e.g. "session expires in 4m"). + */ +class JwtService { + decode(token) { + const parts = token.split('.'); + if (parts.length !== 3) { + return null; + } + try { + const payload = this.base64UrlDecode(parts[1]); + const claims = JSON.parse(payload); + return this.isJwtClaims(claims) ? claims : null; + } + catch { + return null; + } + } + isExpired(claims, skewSeconds = 0) { + return claims.exp * 1000 <= Date.now() + skewSeconds * 1000; + } + isJwtClaims(value) { + if (!value || typeof value !== 'object') { + return false; + } + const claims = value; + return typeof claims.sub === 'string' && typeof claims.role === 'string' && typeof claims.exp === 'number'; + } + base64UrlDecode(input) { + const base64 = input.replace(/-/g, '+').replace(/_/g, '/').padEnd(input.length + ((4 - (input.length % 4)) % 4), '='); + return decodeURIComponent(escape(atob(base64))); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: JwtService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } + static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: JwtService, providedIn: 'root' }); } +} +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: JwtService, decorators: [{ + type: Injectable, + args: [{ providedIn: 'root' }] + }] }); + +const TOKEN_STORAGE_KEY = 'ed25519AdminToken'; +const REFRESH_STORAGE_KEY = 'ed25519AdminRefreshToken'; +/** Refresh this long before actual expiry, so a request never races an expiring token. */ +const REFRESH_SKEW_MS = 60_000; +/** + * Holds the Ed25519-flow JWT/refresh-token pair and derived claims. Separate + * from the telegram module's AdminAuthService (Telegram-session state) by + * design - the two auth mechanisms are not merged until both ship on the + * same backend and a migration decision is made. + */ +class SessionService { + constructor() { + this.jwt = new JwtService(); + this.tokenSignal = signal(null, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "tokenSignal" }] : /* istanbul ignore next */ [])); + this.refreshTokenSignal = signal(null, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "refreshTokenSignal" }] : /* istanbul ignore next */ [])); + this.claimsSignal = signal(null, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "claimsSignal" }] : /* istanbul ignore next */ [])); + this.statusSignal = signal('unknown', /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "statusSignal" }] : /* istanbul ignore next */ [])); + this.token = this.tokenSignal.asReadonly(); + this.claims = this.claimsSignal.asReadonly(); + this.status = this.statusSignal.asReadonly(); + this.isAuthenticated = computed(() => this.statusSignal() === 'authenticated', /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : /* istanbul ignore next */ [])); + this.role = computed(() => this.claimsSignal()?.role ?? null, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "role" }] : /* istanbul ignore next */ [])); + } + /** Called once by AuthService on init to wire up the refresh trigger without a circular DI dependency. */ + onRefreshDue(callback) { + this.refreshCallback = callback; + } + /** Restores session state from persisted storage. Returns true if a (possibly expired) session was found. */ + restore() { + this.statusSignal.set('restoring'); + const token = this.readStorage(TOKEN_STORAGE_KEY); + const refreshToken = this.readStorage(REFRESH_STORAGE_KEY); + if (!token || !refreshToken) { + this.statusSignal.set('unauthenticated'); + return false; + } + const claims = this.jwt.decode(token); + if (!claims) { + this.clear(); + return false; + } + this.tokenSignal.set(token); + this.refreshTokenSignal.set(refreshToken); + this.claimsSignal.set(claims); + if (this.jwt.isExpired(claims)) { + this.statusSignal.set('expired'); + } + else { + this.statusSignal.set('authenticated'); + this.scheduleRefresh(claims); + } + return true; + } + activate(tokens) { + const claims = this.jwt.decode(tokens.token); + if (!claims) { + throw new Error('Received a malformed JWT from the auth backend.'); + } + this.tokenSignal.set(tokens.token); + this.refreshTokenSignal.set(tokens.refreshToken); + this.claimsSignal.set(claims); + this.statusSignal.set('authenticated'); + this.writeStorage(TOKEN_STORAGE_KEY, tokens.token); + this.writeStorage(REFRESH_STORAGE_KEY, tokens.refreshToken); + this.scheduleRefresh(claims); + } + getRefreshToken() { + return this.refreshTokenSignal(); + } + markExpired() { + this.statusSignal.set('expired'); + this.clearRefreshTimer(); + } + clear() { + this.tokenSignal.set(null); + this.refreshTokenSignal.set(null); + this.claimsSignal.set(null); + this.statusSignal.set('unauthenticated'); + this.removeStorage(TOKEN_STORAGE_KEY); + this.removeStorage(REFRESH_STORAGE_KEY); + this.clearRefreshTimer(); + } + scheduleRefresh(claims) { + this.clearRefreshTimer(); + const expiresInMs = claims.exp * 1000 - Date.now(); + const refreshInMs = Math.max(expiresInMs - REFRESH_SKEW_MS, 5_000); + this.refreshTimer = setTimeout(() => this.refreshCallback?.(), refreshInMs); + } + clearRefreshTimer() { + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + this.refreshTimer = undefined; + } + } + readStorage(key) { + return typeof localStorage === 'undefined' ? null : localStorage.getItem(key); + } + writeStorage(key, value) { + if (typeof localStorage !== 'undefined') { + localStorage.setItem(key, value); + } + } + removeStorage(key) { + if (typeof localStorage !== 'undefined') { + localStorage.removeItem(key); + } + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: SessionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } + static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: SessionService, providedIn: 'root' }); } +} +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: SessionService, decorators: [{ + type: Injectable, + args: [{ providedIn: 'root' }] + }] }); + +/** + * Orchestrates the Ed25519 challenge/response admin auth flow end to end: + * + * GET /api/admin/auth/challenge -> { nonce } + * sign(nonce) with local Ed25519 key -> signature + * POST /api/admin/auth/verify -> { token, refreshToken } + * + * This is the lowest-level orchestrator; components should go through + * AuthFacade rather than calling this directly. Exported from the package + * barrel as `Ed25519AuthService` to avoid colliding with the telegram + * module's `AuthService`. + */ +class AuthService { + constructor() { + this.api = inject(AuthApiService); + this.keypair = inject(Ed25519KeypairService); + this.session = inject(SessionService); + this.loginPhaseSignal = signal('idle', /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "loginPhaseSignal" }] : /* istanbul ignore next */ [])); + this.lastErrorSignal = signal(null, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "lastErrorSignal" }] : /* istanbul ignore next */ [])); + this.loginPhase = this.loginPhaseSignal.asReadonly(); + this.lastError = this.lastErrorSignal.asReadonly(); + this.session.onRefreshDue(() => this.refresh().subscribe()); + } + /** Restores a persisted session on app bootstrap. Call once from an APP_INITIALIZER or root component. */ + restoreSession() { + this.session.restore(); + } + login() { + this.lastErrorSignal.set(null); + this.loginPhaseSignal.set('requesting-challenge'); + return this.api.requestChallenge().pipe(switchMap(challenge => this.signChallenge(challenge.nonce).pipe(switchMap(({ publicKeyBase64, signature }) => { + this.loginPhaseSignal.set('verifying'); + return this.api.verifySignature({ publicKey: publicKeyBase64, signature, nonce: challenge.nonce }); + }))), tap(tokens => { + this.session.activate(tokens); + this.loginPhaseSignal.set('done'); + }), catchError(error => this.handleAuthError(error, 'invalid-signature'))); + } + refresh() { + const refreshToken = this.session.getRefreshToken(); + if (!refreshToken) { + this.session.markExpired(); + return throwError(() => this.toAuthError({ code: 'session-expired', message: 'No refresh token available.' })); + } + return this.api.refresh({ refreshToken }).pipe(tap(tokens => this.session.activate(tokens)), catchError(error => this.handleAuthError(error, 'session-expired', () => this.session.markExpired()))); + } + logout() { + const refreshToken = this.session.getRefreshToken(); + this.session.clear(); + if (!refreshToken) { + return new Observable(subscriber => { + subscriber.next(); + subscriber.complete(); + }); + } + return this.api.logout(refreshToken).pipe(catchError(() => throwError(() => null))); + } + signChallenge(nonce) { + this.loginPhaseSignal.set('signing'); + return new Observable(subscriber => { + this.keypair + .getOrCreateKeyPair() + .then(({ publicKeyBase64 }) => this.keypair.sign(nonce).then(signature => { + subscriber.next({ publicKeyBase64, signature }); + subscriber.complete(); + })) + .catch(error => subscriber.error(error)); + }); + } + handleAuthError(error, fallbackCode, onError) { + onError?.(); + return throwError(() => this.toAuthError(this.toAuthErrorShape(error, fallbackCode))); + } + toAuthErrorShape(error, fallbackCode) { + if (error instanceof HttpErrorResponse) { + const bodyCode = error.error?.error?.code; + const code = authErrorCodeFromBackendCode(bodyCode) ?? authErrorCodeFromStatus(error.status); + return { code, message: error.message, status: error.status }; + } + if (error instanceof Error) { + return { code: fallbackCode, message: error.message }; + } + return { code: fallbackCode, message: 'Unknown authentication error.' }; + } + toAuthError(error) { + this.lastErrorSignal.set(error); + return error; + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } + static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthService, providedIn: 'root' }); } +} +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthService, decorators: [{ + type: Injectable, + args: [{ providedIn: 'root' }] + }], ctorParameters: () => [] }); + +const ROLE_PERMISSIONS = { + Owner: ['backoffice.read', 'backoffice.write', 'builder.read', 'builder.write', 'users.manage', 'settings.manage'], + Administrator: ['backoffice.read', 'backoffice.write', 'builder.read', 'builder.write', 'users.manage'], + Editor: ['backoffice.read', 'backoffice.write', 'builder.read', 'builder.write'], + Support: ['backoffice.read'], + ReadOnly: ['backoffice.read', 'builder.read'] +}; + +/** + * Derives the current admin's permission set from their JWT `role` claim. + * UI-only gate (hide/disable) - the backend must independently enforce + * every mutation server-side. + */ +class PermissionService { + constructor() { + this.session = inject(SessionService); + this.permissions = computed(() => { + const role = this.session.role(); + return role ? ROLE_PERMISSIONS[role] : []; + }, /* @ts-ignore */ + ...(ngDevMode ? [{ debugName: "permissions" }] : /* istanbul ignore next */ [])); + } + has(permission) { + return this.permissions().includes(permission); + } + hasAny(permissions) { + return permissions.some(permission => this.has(permission)); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PermissionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } + static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PermissionService, providedIn: 'root' }); } +} +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PermissionService, decorators: [{ + type: Injectable, + args: [{ providedIn: 'root' }] + }] }); + +/** + * Public surface for components/pages. Components should depend on this, + * not on AuthService/SessionService/PermissionService directly, so the + * orchestration details (which service owns what) can change without + * touching UI code. + */ +class AuthFacade { + constructor() { + this.auth = inject(AuthService); + this.session = inject(SessionService); + this.permissions = inject(PermissionService); + this.router = inject(Router); + this.isAuthenticated = this.session.isAuthenticated; + this.status = this.session.status; + this.role = this.session.role; + this.loginPhase = this.auth.loginPhase; + this.lastError = this.auth.lastError; + } + restoreSession() { + this.auth.restoreSession(); + } + login(onSuccessRedirectTo) { + this.auth.login().subscribe({ + next: () => { + if (onSuccessRedirectTo) { + this.router.navigateByUrl(onSuccessRedirectTo); + } + }, + error: () => { + const code = this.auth.lastError()?.code ?? 'unauthorized'; + this.router.navigate(['/admin-login/error', code]); + } + }); + } + logout(redirectTo = '/admin-login') { + this.auth + .logout() + .pipe(finalize(() => this.router.navigateByUrl(redirectTo))) + .subscribe({ error: () => undefined }); + } + can(permission) { + return this.permissions.has(permission); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthFacade, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } + static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthFacade, providedIn: 'root' }); } +} +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthFacade, decorators: [{ + type: Injectable, + args: [{ providedIn: 'root' }] + }] }); + +class Ed25519VerificationService { +} + +/** + * Default DI binding for Ed25519VerificationService until the backend ships + * the real challenge/verify endpoints. Intentionally fails closed (throws) + * rather than pretending to verify anything, so accidental use in a login + * path is loud instead of silently accepting unsigned sessions. + */ +class NoopEd25519VerificationService { + requestChallenge() { + return throwError(() => new Error('Ed25519 challenge endpoint is not yet available from the backend.')); + } + verify(_response) { + return throwError(() => new Error('Ed25519 verification endpoint is not yet available from the backend.')); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: NoopEd25519VerificationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } + static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: NoopEd25519VerificationService, providedIn: 'root' }); } +} +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: NoopEd25519VerificationService, decorators: [{ + type: Injectable, + args: [{ providedIn: 'root' }] + }] }); + +// @marketplaces/auth — public API barrel. +// Two independent auth mechanisms, per ADR-0001 (marketplaces repo: +// docs/context/adrs/ADR-0001-extract-auth-and-payment-into-shared-marketplaces-packages.md): +// - telegram/ — live Telegram QR/session auth (customer + admin) +// - ed25519/ — future Ed25519 challenge/response admin auth (backend not shipped yet) +// Provide AUTH_API_URL (and optionally TELEGRAM_BOT_USERNAME) from the consuming app's config. + +/** + * Generated bundle index. Do not edit. + */ + +export { AUTH_API_URL, AdminAuthService, AuthApiService, AuthFacade, AuthMarketplaceContext, AuthService$1 as AuthService, AuthService as Ed25519AuthService, Ed25519KeypairService, Ed25519VerificationService, HttpMarketplacesAuthGateway, JwtService, MARKETPLACES_AUTH_CONFIG, MARKETPLACES_AUTH_GATEWAY, MARKETPLACE_DOMAIN_HEADER, MarketplacesAuthComponent, NoopEd25519VerificationService, PermissionService, ROLE_PERMISSIONS, SessionService, TELEGRAM_BOT_USERNAME, TelegramSessionApiService, adminAuthGuard, adminAuthHeadersInterceptor, authErrorCodeFromBackendCode, authErrorCodeFromStatus, normalizeMarketplaceDomain, provideMarketplacesAuth }; +//# sourceMappingURL=marketplaces-auth.mjs.map diff --git a/dist/fesm2022/marketplaces-auth.mjs.map b/dist/fesm2022/marketplaces-auth.mjs.map new file mode 100644 index 0000000..d0686d5 --- /dev/null +++ b/dist/fesm2022/marketplaces-auth.mjs.map @@ -0,0 +1 @@ +{"version":3,"file":"marketplaces-auth.mjs","sources":["../../src/config.ts","../../src/marketplace-context.ts","../../src/util/guid.util.ts","../../src/telegram/telegram-session-api.service.ts","../../src/telegram/admin-auth.service.ts","../../src/telegram/auth.service.ts","../../src/ui/auth.gateway.ts","../../src/ui/auth.component.ts","../../src/telegram/admin-auth.guard.ts","../../src/telegram/admin-auth-headers.interceptor.ts","../../src/ed25519/models/auth-error.model.ts","../../src/ed25519/auth-api.service.ts","../../src/ed25519/ed25519-keypair.service.ts","../../src/ed25519/jwt.service.ts","../../src/ed25519/session.service.ts","../../src/ed25519/auth.service.ts","../../src/ed25519/models/permission.model.ts","../../src/ed25519/permission.service.ts","../../src/ed25519/auth-facade.service.ts","../../src/ed25519/ed25519-verification.model.ts","../../src/ed25519/noop-ed25519-verification.service.ts","../../src/index.ts","../../src/marketplaces-auth.ts"],"sourcesContent":["import { EnvironmentProviders, InjectionToken, makeEnvironmentProviders } from '@angular/core';\n\nexport interface MarketplacesAuthConfig {\n /** Central auth service URL. It is not the tenant API URL. */\n apiUrl: string;\n /** Override only for SSR/custom-domain integrations. Browser default is location.hostname. */\n marketplaceDomain?: string | (() => string);\n telegramBotUsername?: string;\n credentialsPath?: string;\n yandexStartPath?: string;\n yandexSessionPath?: string;\n pollIntervalMs?: number;\n}\n\r\n/** Base URL for the auth backend, e.g. `https://api.example.com`. Provide from the consuming app's environment config. */\r\nexport const AUTH_API_URL = new InjectionToken('@marketplaces/auth AUTH_API_URL');\r\n\r\n/** Telegram bot username used to build QR/deep-link login URLs. Optional — falls back to a default if not provided. */\r\nexport const TELEGRAM_BOT_USERNAME = new InjectionToken('@marketplaces/auth TELEGRAM_BOT_USERNAME');\n\nexport const MARKETPLACES_AUTH_CONFIG = new InjectionToken('@marketplaces/auth config');\n\nexport function provideMarketplacesAuth(config: MarketplacesAuthConfig): EnvironmentProviders {\n const normalized: MarketplacesAuthConfig = {\n ...config,\n apiUrl: config.apiUrl.replace(/\\/$/, ''),\n credentialsPath: config.credentialsPath ?? '/auth/credentials/login',\n yandexStartPath: config.yandexStartPath ?? '/auth/yandex/sessions',\n yandexSessionPath: config.yandexSessionPath ?? '/auth/yandex/sessions',\n pollIntervalMs: config.pollIntervalMs ?? 1500,\n };\n return makeEnvironmentProviders([\n { provide: MARKETPLACES_AUTH_CONFIG, useValue: normalized },\n { provide: AUTH_API_URL, useValue: normalized.apiUrl },\n ...(normalized.telegramBotUsername\n ? [{ provide: TELEGRAM_BOT_USERNAME, useValue: normalized.telegramBotUsername }]\n : []),\n ]);\n}\n","import { HttpHeaders } from '@angular/common/http';\nimport { Injectable, inject } from '@angular/core';\nimport { MARKETPLACES_AUTH_CONFIG } from './config';\n\nexport const MARKETPLACE_DOMAIN_HEADER = 'X-Marketplace-Domain';\n\nexport function normalizeMarketplaceDomain(domain: string): string {\n return domain.trim().toLowerCase().replace(/\\.$/, '');\n}\n\n@Injectable({ providedIn: 'root' })\nexport class AuthMarketplaceContext {\n private readonly config = inject(MARKETPLACES_AUTH_CONFIG);\n\n domain(): string {\n const configured = this.config.marketplaceDomain;\n const domain = typeof configured === 'function'\n ? configured()\n : configured ?? (typeof location === 'undefined' ? '' : location.hostname);\n return normalizeMarketplaceDomain(domain);\n }\n\n headers(extra?: Record): HttpHeaders {\n const domain = this.domain();\n if (!domain) throw new Error('Marketplace domain cannot be resolved');\n return new HttpHeaders({ [MARKETPLACE_DOMAIN_HEADER]: domain, ...extra });\n }\n}\n","/** RFC4122 v4-ish GUID, using crypto when available. Shared by customer and admin session creation. */\r\nexport function generateGuid(): string {\r\n if (globalThis.crypto?.randomUUID) {\r\n return globalThis.crypto.randomUUID();\r\n }\r\n\r\n const bytes = new Uint8Array(16);\r\n if (globalThis.crypto?.getRandomValues) {\r\n globalThis.crypto.getRandomValues(bytes);\r\n } else {\r\n for (let index = 0; index < bytes.length; index++) {\r\n bytes[index] = Math.floor(Math.random() * 256);\r\n }\r\n }\r\n\r\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\r\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\r\n\r\n const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0'));\r\n return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;\r\n}\r\n","import { Injectable, inject } from '@angular/core';\r\nimport { HttpClient } from '@angular/common/http';\r\nimport { Observable, of, catchError, map } from 'rxjs';\r\nimport { AuthSession, WebSessionStart } from './models/session.model';\r\nimport { AUTH_API_URL, TELEGRAM_BOT_USERNAME } from '../config';\r\nimport { generateGuid } from '../util/guid.util';\nimport { AuthMarketplaceContext } from '../marketplace-context';\n\r\nconst SESSION_MAX_AGE_SECONDS = 60 * 60;\r\nconst DEFAULT_TELEGRAM_BOT_USERNAME = 'DexarSupport_bot';\r\n\r\n/**\r\n * The one Telegram QR/session API (`{authApiUrl}/users/sessions`). Customer\r\n * login (AuthService) and admin login (AdminAuthService) both call this same\r\n * service against this same endpoint - there is no separate admin backend.\r\n * This class only does the HTTP call + response normalization; it holds no\r\n * session state and writes no cookies, so each caller manages its own\r\n * storage/signals independently on top of it.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class TelegramSessionApiService {\r\n private readonly http = inject(HttpClient);\r\n private readonly authApiUrl = inject(AUTH_API_URL);\r\n private readonly telegramBotUsername = inject(TELEGRAM_BOT_USERNAME, { optional: true });\n private readonly marketplaceContext = inject(AuthMarketplaceContext);\n\r\n createSession(): Observable {\r\n const webSessionID = generateGuid();\r\n\r\n return this.http.post>(\r\n `${this.authApiUrl}/users/sessions`,\r\n { webSessionID },\r\n { headers: this.marketplaceContext.headers({ WebSessionID: webSessionID }) }\n ).pipe(\r\n map(response => {\r\n const responseWebSessionID = this.extractSessionId(response, webSessionID);\r\n return {\r\n webSessionID: responseWebSessionID,\r\n url: this.getBotLoginUrl(responseWebSessionID),\r\n };\r\n })\r\n );\r\n }\r\n\r\n checkSessionOnce(webSessionID: string | null): Observable {\r\n if (!webSessionID) {\r\n return of(null);\r\n }\r\n\r\n return this.http.get>(\r\n `${this.authApiUrl}/users/sessions/${encodeURIComponent(webSessionID)}`,\n { headers: this.marketplaceContext.headers() }\n ).pipe(\r\n map(response => this.normalizeWebSession(response, webSessionID)),\r\n catchError(() => of(null))\r\n );\r\n }\r\n\r\n logout(webSessionID: string): Observable {\r\n return this.http.delete(`${this.authApiUrl}/users/sessions/${encodeURIComponent(webSessionID)}`, {\r\n headers: this.marketplaceContext.headers({ WebSessionID: webSessionID })\n }).pipe(catchError(() => of(null)));\r\n }\r\n\r\n getBotLoginUrl(webSessionID: string): string {\r\n return `https://t.me/${this.getBotUsername()}?start=${encodeURIComponent(webSessionID)}`;\r\n }\r\n\r\n getBotAppLoginUrl(webSessionID: string): string {\r\n return `tg://resolve?domain=${encodeURIComponent(this.getBotUsername())}&start=${encodeURIComponent(webSessionID)}`;\r\n }\r\n\r\n private getBotUsername(): string {\r\n return this.telegramBotUsername || DEFAULT_TELEGRAM_BOT_USERNAME;\r\n }\r\n\r\n private normalizeWebSession(response: Record | null, fallbackSessionId: string): AuthSession | null {\r\n if (!response) {\r\n return null;\r\n }\r\n\r\n const user = this.asRecord(this.readFirst(response, ['user', 'User', 'telegramUser', 'TelegramUser'])) ?? response;\r\n const status = this.readFirst(response, [\r\n 'status', 'Status', 'active', 'Active', 'loggedIn', 'LoggedIn',\r\n 'isLoggedIn', 'IsLoggedIn', 'authenticated', 'Authenticated'\r\n ]);\r\n const active = this.isActiveStatus(status);\r\n const sessionId = this.extractSessionId(response, fallbackSessionId);\r\n const username = this.readString(this.readFirst(user, ['username', 'Username']))\r\n ?? this.readString(this.readFirst(response, ['username', 'Username']));\r\n const firstName = this.readString(this.readFirst(user, ['firstName', 'first_name', 'FirstName', 'First_name']));\r\n const lastName = this.readString(this.readFirst(user, ['lastName', 'last_name', 'LastName', 'Last_name']));\r\n const fullName = [firstName, lastName].filter(Boolean).join(' ');\r\n const explicitDisplayName = this.readString(this.readFirst(response, ['displayName', 'DisplayName', 'name', 'Name']))\r\n ?? this.readString(this.readFirst(user, ['displayName', 'DisplayName', 'name', 'Name']));\r\n const displayName = explicitDisplayName ?? username ?? (fullName || 'Telegram User');\r\n const telegramUserId = this.readNumber(this.readFirst(user, ['userId', 'telegramUserId', 'telegramUserID', 'TelegramUserID', 'id', 'ID']))\r\n ?? this.readNumber(this.readFirst(response, ['userId', 'telegramUserId', 'telegramUserID', 'TelegramUserID', 'userID', 'UserID', 'UserId']))\r\n ?? null;\r\n const expiresAt = this.readString(this.readFirst(response, ['expiresAt', 'ExpiresAt', 'expires', 'Expires']))\r\n ?? new Date(Date.now() + SESSION_MAX_AGE_SECONDS * 1000).toISOString();\r\n\r\n return { sessionId, userId: telegramUserId, username, displayName, active, expires: expiresAt };\r\n }\r\n\r\n private extractSessionId(response: Record | null, fallbackSessionId: string): string {\r\n if (!response) {\r\n return fallbackSessionId;\r\n }\r\n return this.readString(this.readFirst(response, [\r\n 'webSessionID', 'WebSessionID', 'webSessionId', 'sessionID', 'SessionID', 'sessionId', 'id', 'ID'\r\n ])) ?? fallbackSessionId;\r\n }\r\n\r\n private readFirst(source: Record, keys: string[]): unknown {\r\n for (const key of keys) {\r\n if (Object.prototype.hasOwnProperty.call(source, key)) {\r\n return source[key];\r\n }\r\n }\r\n return undefined;\r\n }\r\n\r\n private readString(value: unknown): string | null {\r\n if (typeof value === 'string' && value.trim()) {\r\n return value;\r\n }\r\n if (typeof value === 'number' || typeof value === 'bigint') {\r\n return value.toString();\r\n }\r\n return null;\r\n }\r\n\r\n private readNumber(value: unknown): number | null {\r\n if (typeof value === 'number' && Number.isFinite(value)) {\r\n return value;\r\n }\r\n if (typeof value === 'string') {\r\n const parsed = Number(value);\r\n return Number.isFinite(parsed) ? parsed : null;\r\n }\r\n return null;\r\n }\r\n\r\n private asRecord(value: unknown): Record | null {\r\n return value !== null && typeof value === 'object' && !Array.isArray(value)\r\n ? value as Record\r\n : null;\r\n }\r\n\r\n private isActiveStatus(status: unknown): boolean {\r\n if (status === true || status === 1) {\r\n return true;\r\n }\r\n if (typeof status !== 'string') {\r\n return false;\r\n }\r\n return ['true', '1', 'active', 'authenticated', 'confirmed', 'success', 'logged_in'].includes(status.toLowerCase());\r\n }\r\n}\r\n","import { Injectable, signal, computed, inject, isDevMode } from '@angular/core';\r\nimport { Observable, tap } from 'rxjs';\r\nimport { AdminAuthStatus, AuthSession, WebSessionStart } from './models/session.model';\r\nimport { TelegramSessionApiService } from './telegram-session-api.service';\r\n\r\n/**\r\n * Admin login uses the exact same Telegram QR/session API as the customer\r\n * login (TelegramSessionApiService) - there is no separate admin backend\r\n * endpoint, and none should be invented client-side. Only the *storage* is\r\n * kept separate from AuthService, so an admin QR scan never authenticates\r\n * the customer session or vice versa: distinct cookie name, distinct\r\n * signals, distinct guard/interceptor.\r\n *\r\n * Since the session API itself has no concept of \"admin\", the frontend\r\n * cannot tell an admin Telegram session from a regular one. Actual admin\r\n * authorization must be enforced server-side when admin API calls are made\r\n * with the resulting session id - the frontend only decides where to\r\n * *store* the result.\r\n */\r\nconst ADMIN_SESSION_COOKIE = 'adminSessionID';\r\nconst ADMIN_TOKEN_STORAGE_KEY = 'adminToken';\r\nconst ADMIN_REFRESH_STORAGE_KEY = 'adminRefreshToken';\r\nconst ADMIN_SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60;\r\n\r\n@Injectable({ providedIn: 'root' })\r\nexport class AdminAuthService {\r\n private readonly api = inject(TelegramSessionApiService);\r\n\r\n private readonly sessionSignal = signal(null);\r\n private readonly statusSignal = signal('unknown');\r\n private readonly showLoginSignal = signal(false);\r\n\r\n readonly session = this.sessionSignal.asReadonly();\r\n readonly status = this.statusSignal.asReadonly();\r\n readonly isAuthenticated = computed(() => this.statusSignal() === 'authenticated');\r\n readonly showLoginDialog = this.showLoginSignal.asReadonly();\r\n readonly displayName = computed(() => this.sessionSignal()?.displayName ?? null);\r\n\r\n private sessionCheckTimer?: ReturnType;\r\n\r\n constructor() {\r\n this.checkSession();\r\n }\r\n\r\n checkSession(): void {\r\n const webSessionID = this.getStoredAdminSessionID();\r\n if (!webSessionID) {\r\n this.clearAuthState('unauthenticated');\r\n return;\r\n }\r\n\r\n this.statusSignal.set('checking');\r\n this.checkSessionOnce(webSessionID).subscribe(session => {\r\n if (!session?.active) {\r\n this.clearAuthState('unauthenticated');\r\n }\r\n });\r\n }\r\n\r\n /** Check session without mutating internal state beyond activating on success (used for polling). */\r\n checkSessionOnce(webSessionID = this.getStoredAdminSessionID()): Observable {\r\n return this.api.checkSessionOnce(webSessionID).pipe(\r\n tap(session => {\r\n if (session?.active) {\r\n this.activateSession(session);\r\n }\r\n })\r\n );\r\n }\r\n\r\n /** Create a backend web session - identical call to the customer login (TelegramSessionApiService.createSession). */\r\n createWebSession(): Observable {\r\n return this.api.createSession();\r\n }\r\n\r\n getAdminAppLoginUrl(webSessionID: string): string {\r\n return this.api.getBotAppLoginUrl(webSessionID);\r\n }\r\n\r\n onLoginComplete(): void {\r\n this.hideLogin();\r\n if (!this.isAuthenticated()) {\r\n this.checkSession();\r\n }\r\n }\r\n\r\n requestLogin(): void {\r\n this.showLoginSignal.set(true);\r\n }\r\n\r\n /**\r\n * Dev-only shortcut for local testing without a reachable Telegram/session\r\n * backend: fabricates a local session and activates it directly, skipping\r\n * the QR flow entirely. No-ops in production builds (checked via Angular's\r\n * isDevMode() at runtime, not just build-time, so it is safe even if this\r\n * code ships). Never call this from anywhere reachable in a production build.\r\n */\r\n devBypassLogin(): void {\r\n if (!isDevMode()) {\r\n return;\r\n }\r\n this.hideLogin();\r\n this.activateSession({\r\n sessionId: `dev-bypass-${Date.now()}`,\r\n userId: 0,\r\n username: 'dev-admin',\r\n displayName: 'Dev Admin (local bypass)',\r\n active: true,\r\n expires: new Date(Date.now() + 60 * 60 * 1000).toISOString(),\r\n });\r\n }\r\n\r\n hideLogin(): void {\r\n this.showLoginSignal.set(false);\r\n }\r\n\r\n logout(): void {\n const webSessionID = this.sessionSignal()?.sessionId || this.getStoredAdminSessionID();\r\n if (!webSessionID) {\r\n this.clearAuthState('unauthenticated');\r\n return;\r\n }\r\n\r\n this.api.logout(webSessionID).subscribe(() => this.clearAuthState('unauthenticated'));\r\n }\n\n /** Accept a session/tokens returned by credentials or an external provider. */\n acceptSession(session: AuthSession, token?: string, refreshToken?: string): void {\n this.activateSession(session);\n if (token && refreshToken) this.setAdminTokens(token, refreshToken);\n }\n\r\n /** JWT pair storage, reserved for once the backend issues admin access/refresh tokens. Unused until then. */\r\n getAdminToken(): string | null {\r\n return typeof localStorage === 'undefined' ? null : localStorage.getItem(ADMIN_TOKEN_STORAGE_KEY);\r\n }\r\n\r\n setAdminTokens(token: string, refreshToken: string): void {\r\n if (typeof localStorage === 'undefined') {\r\n return;\r\n }\r\n localStorage.setItem(ADMIN_TOKEN_STORAGE_KEY, token);\r\n localStorage.setItem(ADMIN_REFRESH_STORAGE_KEY, refreshToken);\r\n }\r\n\r\n clearAdminTokens(): void {\r\n if (typeof localStorage === 'undefined') {\r\n return;\r\n }\r\n localStorage.removeItem(ADMIN_TOKEN_STORAGE_KEY);\r\n localStorage.removeItem(ADMIN_REFRESH_STORAGE_KEY);\r\n }\r\n\r\n private activateSession(session: AuthSession): void {\r\n this.sessionSignal.set(session);\r\n this.statusSignal.set('authenticated');\r\n this.setStoredAdminSessionID(session.sessionId);\r\n this.scheduleSessionRefresh(session.expires);\r\n }\r\n\r\n private clearAuthState(status: AdminAuthStatus): void {\r\n this.sessionSignal.set(null);\r\n this.statusSignal.set(status);\r\n this.clearStoredAdminSessionID();\r\n this.clearAdminTokens();\r\n this.clearSessionRefresh();\r\n }\r\n\r\n private scheduleSessionRefresh(expiresAt: string): void {\r\n this.clearSessionRefresh();\r\n const expiresMs = new Date(expiresAt).getTime();\r\n const nowMs = Date.now();\r\n const refreshIn = Number.isFinite(expiresMs)\r\n ? Math.max(expiresMs - nowMs - 60_000, 30_000)\r\n : ADMIN_SESSION_COOKIE_MAX_AGE_SECONDS * 1000;\r\n\r\n this.sessionCheckTimer = setTimeout(() => this.checkSession(), refreshIn);\r\n }\r\n\r\n private clearSessionRefresh(): void {\r\n if (this.sessionCheckTimer) {\r\n clearTimeout(this.sessionCheckTimer);\r\n this.sessionCheckTimer = undefined;\r\n }\r\n }\r\n\r\n private getStoredAdminSessionID(): string | null {\r\n if (typeof document === 'undefined') {\r\n return null;\r\n }\r\n const cookie = document.cookie.split('; ').find(row => row.startsWith(`${ADMIN_SESSION_COOKIE}=`));\r\n if (!cookie) {\r\n return null;\r\n }\r\n try {\r\n return decodeURIComponent(cookie.substring(ADMIN_SESSION_COOKIE.length + 1));\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n private setStoredAdminSessionID(webSessionID: string): void {\r\n if (typeof document === 'undefined') {\r\n return;\r\n }\r\n const secure = typeof window !== 'undefined' && window.location.protocol === 'https:' ? '; Secure' : '';\r\n document.cookie = `${ADMIN_SESSION_COOKIE}=${encodeURIComponent(webSessionID)}; Max-Age=${ADMIN_SESSION_COOKIE_MAX_AGE_SECONDS}; Path=/; SameSite=Strict${secure}`;\r\n }\r\n\r\n private clearStoredAdminSessionID(): void {\r\n if (typeof document === 'undefined') {\r\n return;\r\n }\r\n document.cookie = `${ADMIN_SESSION_COOKIE}=; Max-Age=0; Path=/; SameSite=Strict`;\r\n }\r\n}\r\n","import { Injectable, signal, computed, inject } from '@angular/core';\r\nimport { Observable, tap } from 'rxjs';\r\nimport { AuthSession, AuthStatus, WebSessionStart } from './models/session.model';\r\nimport { TelegramSessionApiService } from './telegram-session-api.service';\r\n\r\nconst WEB_SESSION_COOKIE = 'webSessionID';\r\nconst WEB_SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60;\r\n\r\n/** Customer-facing Telegram QR/session auth. Distinct storage/state from AdminAuthService by design. */\r\n@Injectable({\r\n providedIn: 'root'\r\n})\r\nexport class AuthService {\r\n private readonly api = inject(TelegramSessionApiService);\r\n\r\n private sessionSignal = signal(null);\r\n private statusSignal = signal('unknown');\r\n private showLoginSignal = signal(false);\r\n\r\n /** Current auth session */\r\n readonly session = this.sessionSignal.asReadonly();\r\n /** Current auth status */\r\n readonly status = this.statusSignal.asReadonly();\r\n /** Whether user is fully authenticated */\r\n readonly isAuthenticated = computed(() => this.statusSignal() === 'authenticated');\r\n /** Whether to show login dialog */\r\n readonly showLoginDialog = this.showLoginSignal.asReadonly();\r\n /** Display name of authenticated user */\r\n readonly displayName = computed(() => this.sessionSignal()?.displayName ?? null);\r\n\r\n private sessionCheckTimer?: ReturnType;\r\n\r\n constructor() {\r\n // On init, check existing session via cookie\r\n this.checkSession();\r\n }\r\n\r\n /** Check the current webSessionID cookie against the auth backend. */\r\n checkSession(): void {\r\n const webSessionID = this.getStoredWebSessionID();\r\n\r\n if (!webSessionID) {\r\n this.clearAuthState('unauthenticated');\r\n return;\r\n }\r\n\r\n this.statusSignal.set('checking');\r\n\r\n this.checkSessionOnce(webSessionID).subscribe(session => {\r\n if (!session?.active) {\r\n this.clearAuthState('unauthenticated');\r\n }\r\n });\r\n }\r\n\r\n /** Check session without updating internal state beyond activating on success (used for polling). */\r\n checkSessionOnce(webSessionID = this.getStoredWebSessionID()): Observable {\r\n return this.api.checkSessionOnce(webSessionID).pipe(\r\n tap(session => {\r\n if (session?.active) {\r\n this.activateSession(session);\r\n }\r\n })\r\n );\r\n }\r\n\r\n /**\r\n * Called after user completes Telegram login.\r\n */\r\n onTelegramLoginComplete(): void {\r\n this.hideLogin();\r\n\r\n if (!this.isAuthenticated()) {\r\n this.checkSession();\r\n }\r\n }\r\n\r\n /** Generate the Telegram login URL for bot-based auth */\r\n getTelegramLoginUrl(webSessionID: string): string {\r\n return this.api.getBotLoginUrl(webSessionID);\r\n }\r\n\r\n /** Generate a Telegram app deep link for mobile login without opening a browser tab. */\r\n getTelegramAppLoginUrl(webSessionID: string): string {\r\n return this.api.getBotAppLoginUrl(webSessionID);\r\n }\r\n\r\n /** Create a backend web session and return the Telegram start link for it. */\r\n createWebSession(): Observable {\r\n return this.api.createSession();\r\n }\r\n\r\n /** Show login dialog (called when user tries to pay without being logged in) */\r\n requestLogin(): void {\r\n this.showLoginSignal.set(true);\r\n }\r\n\r\n /** Hide login dialog */\r\n hideLogin(): void {\r\n this.showLoginSignal.set(false);\r\n }\r\n\r\n /** Logout — clears session on backend and locally */\r\n logout(): void {\n const webSessionID = this.sessionSignal()?.sessionId || this.getStoredWebSessionID();\r\n\r\n if (!webSessionID) {\r\n this.clearAuthState('unauthenticated');\r\n return;\r\n }\r\n\r\n this.api.logout(webSessionID).subscribe(() => {\r\n this.clearAuthState('unauthenticated');\r\n });\r\n }\n\n /** Accept a session returned by credentials or an external provider. */\n acceptSession(session: AuthSession): void {\n this.activateSession(session);\n }\n\r\n private activateSession(session: AuthSession): void {\r\n this.sessionSignal.set(session);\r\n this.statusSignal.set('authenticated');\r\n this.setStoredWebSessionID(session.sessionId);\r\n this.scheduleSessionRefresh(session.expires);\r\n }\r\n\r\n private clearAuthState(status: AuthStatus): void {\r\n this.sessionSignal.set(null);\r\n this.statusSignal.set(status);\r\n this.clearStoredWebSessionID();\r\n this.clearSessionRefresh();\r\n }\r\n\r\n /** Schedule a session re-check before it expires */\r\n private scheduleSessionRefresh(expiresAt: string): void {\r\n this.clearSessionRefresh();\r\n\r\n const expiresMs = new Date(expiresAt).getTime();\r\n const nowMs = Date.now();\r\n // Re-check 60 seconds before expiry, minimum 30s from now\r\n const refreshIn = Number.isFinite(expiresMs)\r\n ? Math.max(expiresMs - nowMs - 60_000, 30_000)\r\n : WEB_SESSION_COOKIE_MAX_AGE_SECONDS * 1000;\r\n\r\n this.sessionCheckTimer = setTimeout(() => {\r\n this.checkSession();\r\n }, refreshIn);\r\n }\r\n\r\n private clearSessionRefresh(): void {\r\n if (this.sessionCheckTimer) {\r\n clearTimeout(this.sessionCheckTimer);\r\n this.sessionCheckTimer = undefined;\r\n }\r\n }\r\n\r\n private getStoredWebSessionID(): string | null {\r\n if (typeof document === 'undefined') {\r\n return null;\r\n }\r\n\r\n const cookie = document.cookie\r\n .split('; ')\r\n .find(row => row.startsWith(`${WEB_SESSION_COOKIE}=`));\r\n\r\n if (!cookie) {\r\n return null;\r\n }\r\n\r\n try {\r\n return decodeURIComponent(cookie.substring(WEB_SESSION_COOKIE.length + 1));\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n private setStoredWebSessionID(webSessionID: string): void {\r\n if (typeof document === 'undefined') {\r\n return;\r\n }\r\n\r\n const secure = typeof window !== 'undefined' && window.location.protocol === 'https:' ? '; Secure' : '';\r\n document.cookie = `${WEB_SESSION_COOKIE}=${encodeURIComponent(webSessionID)}; Max-Age=${WEB_SESSION_COOKIE_MAX_AGE_SECONDS}; Path=/; SameSite=Lax${secure}`;\r\n }\r\n\r\n private clearStoredWebSessionID(): void {\r\n if (typeof document === 'undefined') {\r\n return;\r\n }\r\n\r\n document.cookie = `${WEB_SESSION_COOKIE}=; Max-Age=0; Path=/; SameSite=Lax`;\r\n }\r\n}\r\n","import { HttpClient, HttpErrorResponse } from '@angular/common/http';\nimport { Injectable, InjectionToken, inject } from '@angular/core';\nimport { Observable, catchError, map, of, throwError } from 'rxjs';\nimport { MARKETPLACES_AUTH_CONFIG } from '../config';\nimport { AuthMarketplaceContext } from '../marketplace-context';\nimport { AdminAuthService } from '../telegram/admin-auth.service';\nimport { AuthService } from '../telegram/auth.service';\nimport { AuthSession, WebSessionStart } from '../telegram/models/session.model';\nimport { AuthFailure, AuthMode, AuthResult, CredentialLogin, ExternalAuthStart } from './auth.models';\n\nexport interface MarketplacesAuthGateway {\n startQr(mode: AuthMode): Observable;\n checkQr(mode: AuthMode, attemptId: string): Observable;\n loginWithCredentials(mode: AuthMode, credentials: CredentialLogin): Observable;\n startYandex(mode: AuthMode, returnUrl: string): Observable;\n checkYandex(mode: AuthMode, attemptId: string): Observable;\n}\n\nexport const MARKETPLACES_AUTH_GATEWAY = new InjectionToken(\n '@marketplaces/auth gateway',\n { providedIn: 'root', factory: () => inject(HttpMarketplacesAuthGateway) }\n);\n\n@Injectable({ providedIn: 'root' })\nexport class HttpMarketplacesAuthGateway implements MarketplacesAuthGateway {\n private readonly http = inject(HttpClient);\n private readonly config = inject(MARKETPLACES_AUTH_CONFIG);\n private readonly context = inject(AuthMarketplaceContext);\n private readonly customerAuth = inject(AuthService);\n private readonly adminAuth = inject(AdminAuthService);\n\n startQr(mode: AuthMode): Observable {\n return mode === 'admin' ? this.adminAuth.createWebSession() : this.customerAuth.createWebSession();\n }\n checkQr(mode: AuthMode, attemptId: string): Observable {\n return mode === 'admin' ? this.adminAuth.checkSessionOnce(attemptId) : this.customerAuth.checkSessionOnce(attemptId);\n }\n loginWithCredentials(mode: AuthMode, credentials: CredentialLogin): Observable {\n return this.http.post(this.url(this.config.credentialsPath), { ...credentials, mode }, {\n headers: this.context.headers(),\n }).pipe(\n map(result => this.accept(mode, { ...result, method: 'credentials', mode })),\n catchError(error => throwError(() => this.failure('credentials', error)))\n );\n }\n startYandex(mode: AuthMode, returnUrl: string): Observable {\n return this.http.post(this.url(this.config.yandexStartPath), {\n provider: 'yandex', mode, returnUrl,\n }, { headers: this.context.headers() }).pipe(\n catchError(error => throwError(() => this.failure('yandex', error)))\n );\n }\n checkYandex(mode: AuthMode, attemptId: string): Observable {\n return this.http.get(\n `${this.url(this.config.yandexSessionPath)}/${encodeURIComponent(attemptId)}`,\n { headers: this.context.headers() }\n ).pipe(\n map(result => result ? this.accept(mode, { ...result, method: 'yandex', mode }) : null),\n catchError((error: HttpErrorResponse) => error.status === 404 || error.status === 202\n ? of(null)\n : throwError(() => this.failure('yandex', error)))\n );\n }\n private accept(mode: AuthMode, result: AuthResult): AuthResult {\n if (mode === 'admin') this.adminAuth.acceptSession(result.session, result.accessToken, result.refreshToken);\n else this.customerAuth.acceptSession(result.session);\n return result;\n }\n private url(path = ''): string { return `${this.config.apiUrl}${path.startsWith('/') ? path : `/${path}`}`; }\n private failure(method: 'credentials' | 'yandex', cause: unknown): AuthFailure {\n const response = cause instanceof HttpErrorResponse ? cause : null;\n return {\n method,\n code: response?.status === 401 ? 'invalid_credentials' : 'backend',\n message: response?.error?.message || response?.message || 'Authentication failed',\n cause,\n };\n }\n}\n","import { Component, DestroyRef, booleanAttribute, inject, input, output, signal } from '@angular/core';\nimport { FormField, form, required } from '@angular/forms/signals';\nimport * as QRCode from 'qrcode';\nimport { Subscription, switchMap, timer } from 'rxjs';\nimport { MARKETPLACES_AUTH_CONFIG } from '../config';\nimport { MARKETPLACES_AUTH_GATEWAY } from './auth.gateway';\nimport { AuthFailure, AuthMethod, AuthMode, AuthResult } from './auth.models';\n\n@Component({\n selector: 'mp-auth, marketplaces-auth',\n standalone: true,\n imports: [FormField],\n template: `\n
\n

{{ title() }}

\n
\n @if (qr()) { }\n @if (credentials()) { }\n @if (yandex()) { }\n
\n @if (method() === 'credentials') {\n
\n \n \n \n
\n }\n @if (method() === 'qr') {\n @if (qrImage()) { \"QR-код }\n \n }\n @if (method() === 'yandex') { }\n @if (busy()) {

Ожидаем подтверждение…

}\n @if (error()) {

{{ error()!.message }}

}\n
\n `,\n styles: [`\n :host{display:block}.mp-auth{font:inherit;color:inherit;display:grid;gap:1rem;max-width:25rem}\n h2,p{margin:0}.methods{display:flex;gap:.5rem;flex-wrap:wrap}.methods button{background:transparent;color:inherit}\n button,input{font:inherit;border:1px solid #c7c7c7;border-radius:.65rem;padding:.7rem .9rem}\n button{cursor:pointer}.active,button[type=submit]{background:#111;color:#fff;border-color:#111}\n button:disabled{opacity:.55;cursor:wait}form{display:grid;gap:.8rem}label{display:grid;gap:.35rem}\n img{display:block;width:min(15rem,100%);height:auto;border-radius:.75rem}.error{color:#b42318}\n `],\n})\nexport class MarketplacesAuthComponent {\n readonly qr = input(false, { transform: booleanAttribute });\n readonly credentials = input(false, { transform: booleanAttribute });\n readonly yandex = input(false, { transform: booleanAttribute });\n readonly mode = input('customer');\n readonly title = input('Вход');\n readonly authenticated = output();\n readonly authError = output();\n readonly cancelled = output();\n readonly method = signal(null);\n readonly busy = signal(false);\n readonly error = signal(null);\n readonly qrImage = signal(null);\n readonly externalUrl = signal(null);\n private readonly credentialsModel = signal({ login: '', password: '' });\n readonly credentialsForm = form(this.credentialsModel, path => {\n required(path.login, { message: 'Введите логин' });\n required(path.password, { message: 'Введите пароль' });\n });\n private readonly gateway = inject(MARKETPLACES_AUTH_GATEWAY);\n private readonly config = inject(MARKETPLACES_AUTH_CONFIG);\n private poll?: Subscription;\n\n constructor() {\n inject(DestroyRef).onDestroy(() => this.poll?.unsubscribe());\n queueMicrotask(() => this.select(this.qr() ? 'qr' : this.credentials() ? 'credentials' : this.yandex() ? 'yandex' : null));\n }\n select(method: AuthMethod | null): void { this.poll?.unsubscribe(); this.busy.set(false); this.error.set(null); this.method.set(method); }\n startQr(): void {\n this.begin();\n this.gateway.startQr(this.mode()).subscribe({\n next: attempt => void this.prepareQr(attempt.url, attempt.webSessionID).catch(cause => this.fail('qr', cause)),\n error: cause => this.fail('qr', cause),\n });\n }\n loginWithCredentials(event: Event): void {\n event.preventDefault();\n if (this.credentialsForm().invalid()) return;\n this.begin();\n this.gateway.loginWithCredentials(this.mode(), this.credentialsModel()).subscribe({\n next: result => this.finish(result), error: cause => this.fail('credentials', cause),\n });\n }\n startYandex(): void {\n this.begin();\n const returnUrl = typeof location === 'undefined' ? '' : location.href;\n this.gateway.startYandex(this.mode(), returnUrl).subscribe({\n next: attempt => {\n const popup = typeof window === 'undefined' ? null : window.open(attempt.authorizationUrl, 'mp-yandex-auth', 'popup,width=520,height=720');\n if (!popup) { this.fail('yandex', { method: 'yandex', code: 'popup_blocked', message: 'Браузер заблокировал окно Яндекса' }); return; }\n this.pollForYandex(attempt.attemptId);\n },\n error: cause => this.fail('yandex', cause),\n });\n }\n private pollForQr(attemptId: string): void {\n this.poll?.unsubscribe();\n this.poll = timer(0, this.config.pollIntervalMs ?? 1500).pipe(switchMap(() => this.gateway.checkQr(this.mode(), attemptId)))\n .subscribe({ next: session => { if (session?.active) this.finish({ method: 'qr', mode: this.mode(), session }); }, error: cause => this.fail('qr', cause) });\n }\n private async prepareQr(url: string, attemptId: string): Promise {\n this.externalUrl.set(url);\n this.qrImage.set(await QRCode.toDataURL(url, { width: 320, margin: 1 }));\n this.pollForQr(attemptId);\n }\n private pollForYandex(attemptId: string): void {\n this.poll?.unsubscribe();\n this.poll = timer(0, this.config.pollIntervalMs ?? 1500).pipe(switchMap(() => this.gateway.checkYandex(this.mode(), attemptId)))\n .subscribe({ next: result => { if (result) this.finish(result); }, error: cause => this.fail('yandex', cause) });\n }\n private begin(): void { this.poll?.unsubscribe(); this.error.set(null); this.busy.set(true); }\n private finish(result: AuthResult): void { this.poll?.unsubscribe(); this.busy.set(false); this.authenticated.emit(result); }\n private fail(method: AuthMethod, cause: unknown): void {\n this.poll?.unsubscribe(); this.busy.set(false);\n const failure: AuthFailure = this.isFailure(cause) ? cause : { method, code: 'backend', message: 'Не удалось выполнить вход', cause };\n this.error.set(failure); this.authError.emit(failure);\n }\n private isFailure(value: unknown): value is AuthFailure { return !!value && typeof value === 'object' && 'code' in value && 'message' in value; }\n}\n","import { inject } from '@angular/core';\r\nimport { CanActivateFn } from '@angular/router';\r\nimport { AdminAuthService } from './admin-auth.service';\r\n\r\n/** Guards `/admin/**`-style routes. Never shares state with the customer auth guard/service. */\r\nexport const adminAuthGuard: CanActivateFn = () => {\r\n const adminAuth = inject(AdminAuthService);\r\n\r\n if (adminAuth.isAuthenticated()) {\r\n return true;\r\n }\r\n\r\n adminAuth.requestLogin();\r\n return false;\r\n};\r\n","import { HttpInterceptorFn } from '@angular/common/http';\r\nimport { inject } from '@angular/core';\r\nimport { AdminAuthService } from './admin-auth.service';\r\n\r\n/** Backend paths that require an active AdminWebSessionID. Adjust to match your API surface if consuming this outside marketplaces. */\r\nconst ADMIN_GATED_PATH_SEGMENTS = ['/admin/', '/backoffice/', '/builder/', '/media/'];\r\n\r\n/**\r\n * Attaches admin session/token headers only to admin API requests. Scoped to\r\n * admin-gated paths so it never touches customer requests and never reads\r\n * the customer AuthService's session.\r\n */\r\nexport const adminAuthHeadersInterceptor: HttpInterceptorFn = (req, next) => {\r\n const isAdminRequest = ADMIN_GATED_PATH_SEGMENTS.some(segment => req.url.includes(segment));\r\n if (!isAdminRequest) {\r\n return next(req);\r\n }\r\n\r\n const adminAuth = inject(AdminAuthService);\r\n const session = adminAuth.session();\r\n const token = adminAuth.getAdminToken();\r\n\r\n let headers = req.headers;\r\n if (session?.sessionId) {\r\n headers = headers.set('AdminWebSessionID', session.sessionId);\r\n }\r\n if (token) {\r\n headers = headers.set('Authorization', `Bearer ${token}`);\r\n }\r\n\r\n return next(req.clone({ headers }));\r\n};\r\n","/**\r\n * Error codes the Ed25519 admin auth flow can surface to the UI. Each maps to\r\n * a dedicated screen rather than a generic toast, because the recovery\r\n * action differs per code (re-login vs. retry vs. wait).\r\n */\r\nexport type AuthErrorCode =\r\n | 'session-expired'\r\n | 'invalid-signature'\r\n | 'unauthorized'\r\n | 'forbidden'\r\n | 'backend-unavailable';\r\n\r\nexport interface AuthError {\r\n code: AuthErrorCode;\r\n message: string;\r\n /** HTTP status that produced this error, when known (absent for client-side errors, e.g. no Ed25519 support). */\r\n status?: number;\r\n}\r\n\r\n/** Maps a backend error envelope's `error.code` to the client's AuthErrorCode screens. Only codes with a dedicated screen are mapped; anything else falls back to the HTTP-status-derived code via authErrorCodeFromStatus. */\r\nconst BACKEND_ERROR_CODE_MAP: Record = {\r\n TOKEN_EXPIRED: 'session-expired',\r\n INVALID_SIGNATURE: 'invalid-signature',\r\n UNAUTHENTICATED: 'unauthorized',\r\n FORBIDDEN: 'forbidden',\r\n SERVICE_UNAVAILABLE: 'backend-unavailable',\r\n};\r\n\r\nexport function authErrorCodeFromBackendCode(code: unknown): AuthErrorCode | undefined {\r\n return typeof code === 'string' ? BACKEND_ERROR_CODE_MAP[code] : undefined;\r\n}\r\n\r\n/** Maps a backend HTTP status to the AuthErrorCode screen it should route to. */\r\nexport function authErrorCodeFromStatus(status: number): AuthErrorCode {\r\n switch (status) {\r\n case 401:\r\n return 'unauthorized';\r\n case 403:\r\n return 'forbidden';\r\n case 0:\r\n return 'backend-unavailable';\r\n default:\r\n return status >= 500 ? 'backend-unavailable' : 'unauthorized';\r\n }\r\n}\r\n","import { HttpClient } from '@angular/common/http';\r\nimport { Injectable, inject } from '@angular/core';\r\nimport { Observable } from 'rxjs';\r\nimport { AUTH_API_URL } from '../config';\r\nimport { AuthChallenge, AuthTokenPair, RefreshTokenRequest, VerifySignatureRequest } from './models/auth-api.model';\r\n\r\n/**\r\n * Thin HTTP client for the Ed25519 admin auth endpoints. These endpoints may\r\n * not exist on every backend yet - calling them before the backend ships\r\n * 404s or connection-errors, which AuthService maps to the\r\n * `backend-unavailable` error screen. No mock/fake responses are fabricated\r\n * here; this is real HttpClient wiring against the real contract.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class AuthApiService {\r\n private readonly http = inject(HttpClient);\r\n private readonly baseUrl = `${inject(AUTH_API_URL)}/api/admin/auth`;\r\n\r\n requestChallenge(): Observable {\r\n return this.http.get(`${this.baseUrl}/challenge`);\r\n }\r\n\r\n verifySignature(request: VerifySignatureRequest): Observable {\r\n return this.http.post(`${this.baseUrl}/verify`, request);\r\n }\r\n\r\n refresh(request: RefreshTokenRequest): Observable {\r\n return this.http.post(`${this.baseUrl}/refresh`, request);\r\n }\r\n\r\n logout(refreshToken: string): Observable {\r\n return this.http.post(`${this.baseUrl}/logout`, { refreshToken } satisfies RefreshTokenRequest);\r\n }\r\n}\r\n","import { Injectable } from '@angular/core';\r\n\r\n/**\r\n * Manages the browser-local Ed25519 keypair used to sign admin auth\r\n * challenges. Real WebCrypto Ed25519 (RFC 8032 support landed in evergreen\r\n * browsers) - not a placeholder. The private key is generated\r\n * non-extractable and kept only in IndexedDB as a CryptoKey handle; it is\r\n * never serialized, never sent anywhere, and cannot be exported by design.\r\n *\r\n * Registering `publicKey` with an admin's account (associating it with a\r\n * role) is a backend-side, out-of-band operation (e.g. an Owner approving a\r\n * new admin's public key) - entirely outside this frontend's scope.\r\n */\r\nconst DB_NAME = 'admin-auth-ed25519';\r\nconst DB_VERSION = 1;\r\nconst STORE_NAME = 'keypair';\r\nconst KEY_RECORD_ID = 'device-keypair';\r\n\r\ninterface StoredKeyPair {\r\n id: string;\r\n publicKey: CryptoKey;\r\n privateKey: CryptoKey;\r\n publicKeyBase64: string;\r\n}\r\n\r\n@Injectable({ providedIn: 'root' })\r\nexport class Ed25519KeypairService {\r\n private cached: StoredKeyPair | null = null;\r\n\r\n isSupported(): boolean {\r\n return typeof crypto !== 'undefined' && !!crypto.subtle && typeof indexedDB !== 'undefined';\r\n }\r\n\r\n /** Returns the device's Ed25519 keypair, generating and persisting one on first use. */\r\n async getOrCreateKeyPair(): Promise<{ publicKeyBase64: string }> {\r\n if (!this.isSupported()) {\r\n throw new Error('Ed25519 is not supported in this browser (requires WebCrypto + IndexedDB).');\r\n }\r\n const existing = await this.loadFromStore();\r\n if (existing) {\r\n this.cached = existing;\r\n return { publicKeyBase64: existing.publicKeyBase64 };\r\n }\r\n\r\n const generated = await this.generateAndPersist();\r\n this.cached = generated;\r\n return { publicKeyBase64: generated.publicKeyBase64 };\r\n }\r\n\r\n async sign(message: string): Promise {\r\n const keyPair = this.cached ?? (await this.loadFromStore());\r\n if (!keyPair) {\r\n throw new Error('No Ed25519 keypair available - call getOrCreateKeyPair() first.');\r\n }\r\n\r\n const signatureBuffer = await crypto.subtle.sign('Ed25519', keyPair.privateKey, new TextEncoder().encode(message));\r\n return this.toBase64(new Uint8Array(signatureBuffer));\r\n }\r\n\r\n /** Discards the local keypair (e.g. \"forget this device\"). A new keypair on next login requires re-registration with the backend. */\r\n async clear(): Promise {\r\n this.cached = null;\r\n const db = await this.openDatabase();\r\n await new Promise((resolve, reject) => {\r\n const tx = db.transaction(STORE_NAME, 'readwrite');\r\n tx.objectStore(STORE_NAME).delete(KEY_RECORD_ID);\r\n tx.oncomplete = () => resolve();\r\n tx.onerror = () => reject(tx.error);\r\n });\r\n }\r\n\r\n private async generateAndPersist(): Promise {\r\n const keyPair = (await crypto.subtle.generateKey({ name: 'Ed25519' }, false, ['sign', 'verify'])) as CryptoKeyPair;\r\n const publicKeyRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey);\r\n const publicKeyBase64 = this.toBase64(new Uint8Array(publicKeyRaw));\r\n\r\n const record: StoredKeyPair = {\r\n id: KEY_RECORD_ID,\r\n publicKey: keyPair.publicKey,\r\n privateKey: keyPair.privateKey,\r\n publicKeyBase64\r\n };\r\n\r\n const db = await this.openDatabase();\r\n await new Promise((resolve, reject) => {\r\n const tx = db.transaction(STORE_NAME, 'readwrite');\r\n tx.objectStore(STORE_NAME).put(record);\r\n tx.oncomplete = () => resolve();\r\n tx.onerror = () => reject(tx.error);\r\n });\r\n\r\n return record;\r\n }\r\n\r\n private async loadFromStore(): Promise {\r\n const db = await this.openDatabase();\r\n return new Promise((resolve, reject) => {\r\n const tx = db.transaction(STORE_NAME, 'readonly');\r\n const request = tx.objectStore(STORE_NAME).get(KEY_RECORD_ID);\r\n request.onsuccess = () => resolve((request.result as StoredKeyPair | undefined) ?? null);\r\n request.onerror = () => reject(request.error);\r\n });\r\n }\r\n\r\n private openDatabase(): Promise {\r\n return new Promise((resolve, reject) => {\r\n const request = indexedDB.open(DB_NAME, DB_VERSION);\r\n request.onupgradeneeded = () => {\r\n if (!request.result.objectStoreNames.contains(STORE_NAME)) {\r\n request.result.createObjectStore(STORE_NAME, { keyPath: 'id' });\r\n }\r\n };\r\n request.onsuccess = () => resolve(request.result);\r\n request.onerror = () => reject(request.error);\r\n });\r\n }\r\n\r\n private toBase64(bytes: Uint8Array): string {\r\n let binary = '';\r\n for (const byte of bytes) {\r\n binary += String.fromCharCode(byte);\r\n }\r\n return btoa(binary);\r\n }\r\n}\r\n","import { Injectable } from '@angular/core';\r\nimport { JwtClaims } from './models/auth-api.model';\r\n\r\n/**\r\n * Client-side JWT *decoding* only - never verification. The signature is\r\n * meaningless to check here because the frontend has no trusted key to check\r\n * it against; verifying a JWT's signature is the backend's job on every\r\n * request. This service exists purely so the UI can read `role`/`exp` for\r\n * display and route-gating UX (e.g. \"session expires in 4m\").\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class JwtService {\r\n decode(token: string): JwtClaims | null {\r\n const parts = token.split('.');\r\n if (parts.length !== 3) {\r\n return null;\r\n }\r\n\r\n try {\r\n const payload = this.base64UrlDecode(parts[1]);\r\n const claims = JSON.parse(payload) as JwtClaims;\r\n return this.isJwtClaims(claims) ? claims : null;\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n isExpired(claims: JwtClaims, skewSeconds = 0): boolean {\r\n return claims.exp * 1000 <= Date.now() + skewSeconds * 1000;\r\n }\r\n\r\n private isJwtClaims(value: unknown): value is JwtClaims {\r\n if (!value || typeof value !== 'object') {\r\n return false;\r\n }\r\n const claims = value as Partial;\r\n return typeof claims.sub === 'string' && typeof claims.role === 'string' && typeof claims.exp === 'number';\r\n }\r\n\r\n private base64UrlDecode(input: string): string {\r\n const base64 = input.replace(/-/g, '+').replace(/_/g, '/').padEnd(input.length + ((4 - (input.length % 4)) % 4), '=');\r\n return decodeURIComponent(escape(atob(base64)));\r\n }\r\n}\r\n","import { Injectable, computed, signal } from '@angular/core';\r\nimport { AuthTokenPair, JwtClaims } from './models/auth-api.model';\r\nimport { JwtService } from './jwt.service';\r\n\r\nexport type SessionStatus = 'unknown' | 'restoring' | 'authenticated' | 'unauthenticated' | 'expired';\r\n\r\nconst TOKEN_STORAGE_KEY = 'ed25519AdminToken';\r\nconst REFRESH_STORAGE_KEY = 'ed25519AdminRefreshToken';\r\n/** Refresh this long before actual expiry, so a request never races an expiring token. */\r\nconst REFRESH_SKEW_MS = 60_000;\r\n\r\n/**\r\n * Holds the Ed25519-flow JWT/refresh-token pair and derived claims. Separate\r\n * from the telegram module's AdminAuthService (Telegram-session state) by\r\n * design - the two auth mechanisms are not merged until both ship on the\r\n * same backend and a migration decision is made.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class SessionService {\r\n private readonly jwt = new JwtService();\r\n\r\n private readonly tokenSignal = signal(null);\r\n private readonly refreshTokenSignal = signal(null);\r\n private readonly claimsSignal = signal(null);\r\n private readonly statusSignal = signal('unknown');\r\n\r\n readonly token = this.tokenSignal.asReadonly();\r\n readonly claims = this.claimsSignal.asReadonly();\r\n readonly status = this.statusSignal.asReadonly();\r\n readonly isAuthenticated = computed(() => this.statusSignal() === 'authenticated');\r\n readonly role = computed(() => this.claimsSignal()?.role ?? null);\r\n\r\n private refreshTimer?: ReturnType;\r\n private refreshCallback?: () => void;\r\n\r\n /** Called once by AuthService on init to wire up the refresh trigger without a circular DI dependency. */\r\n onRefreshDue(callback: () => void): void {\r\n this.refreshCallback = callback;\r\n }\r\n\r\n /** Restores session state from persisted storage. Returns true if a (possibly expired) session was found. */\r\n restore(): boolean {\r\n this.statusSignal.set('restoring');\r\n const token = this.readStorage(TOKEN_STORAGE_KEY);\r\n const refreshToken = this.readStorage(REFRESH_STORAGE_KEY);\r\n if (!token || !refreshToken) {\r\n this.statusSignal.set('unauthenticated');\r\n return false;\r\n }\r\n\r\n const claims = this.jwt.decode(token);\r\n if (!claims) {\r\n this.clear();\r\n return false;\r\n }\r\n\r\n this.tokenSignal.set(token);\r\n this.refreshTokenSignal.set(refreshToken);\r\n this.claimsSignal.set(claims);\r\n\r\n if (this.jwt.isExpired(claims)) {\r\n this.statusSignal.set('expired');\r\n } else {\r\n this.statusSignal.set('authenticated');\r\n this.scheduleRefresh(claims);\r\n }\r\n return true;\r\n }\r\n\r\n activate(tokens: AuthTokenPair): void {\r\n const claims = this.jwt.decode(tokens.token);\r\n if (!claims) {\r\n throw new Error('Received a malformed JWT from the auth backend.');\r\n }\r\n\r\n this.tokenSignal.set(tokens.token);\r\n this.refreshTokenSignal.set(tokens.refreshToken);\r\n this.claimsSignal.set(claims);\r\n this.statusSignal.set('authenticated');\r\n this.writeStorage(TOKEN_STORAGE_KEY, tokens.token);\r\n this.writeStorage(REFRESH_STORAGE_KEY, tokens.refreshToken);\r\n this.scheduleRefresh(claims);\r\n }\r\n\r\n getRefreshToken(): string | null {\r\n return this.refreshTokenSignal();\r\n }\r\n\r\n markExpired(): void {\r\n this.statusSignal.set('expired');\r\n this.clearRefreshTimer();\r\n }\r\n\r\n clear(): void {\r\n this.tokenSignal.set(null);\r\n this.refreshTokenSignal.set(null);\r\n this.claimsSignal.set(null);\r\n this.statusSignal.set('unauthenticated');\r\n this.removeStorage(TOKEN_STORAGE_KEY);\r\n this.removeStorage(REFRESH_STORAGE_KEY);\r\n this.clearRefreshTimer();\r\n }\r\n\r\n private scheduleRefresh(claims: JwtClaims): void {\r\n this.clearRefreshTimer();\r\n const expiresInMs = claims.exp * 1000 - Date.now();\r\n const refreshInMs = Math.max(expiresInMs - REFRESH_SKEW_MS, 5_000);\r\n this.refreshTimer = setTimeout(() => this.refreshCallback?.(), refreshInMs);\r\n }\r\n\r\n private clearRefreshTimer(): void {\r\n if (this.refreshTimer) {\r\n clearTimeout(this.refreshTimer);\r\n this.refreshTimer = undefined;\r\n }\r\n }\r\n\r\n private readStorage(key: string): string | null {\r\n return typeof localStorage === 'undefined' ? null : localStorage.getItem(key);\r\n }\r\n\r\n private writeStorage(key: string, value: string): void {\r\n if (typeof localStorage !== 'undefined') {\r\n localStorage.setItem(key, value);\r\n }\r\n }\r\n\r\n private removeStorage(key: string): void {\r\n if (typeof localStorage !== 'undefined') {\r\n localStorage.removeItem(key);\r\n }\r\n }\r\n}\r\n","import { Injectable, inject, signal } from '@angular/core';\r\nimport { HttpErrorResponse } from '@angular/common/http';\r\nimport { catchError, switchMap, tap, throwError } from 'rxjs';\r\nimport { Observable } from 'rxjs';\r\nimport { AuthTokenPair } from './models/auth-api.model';\r\nimport { AuthError, authErrorCodeFromBackendCode, authErrorCodeFromStatus } from './models/auth-error.model';\r\nimport { AuthApiService } from './auth-api.service';\r\nimport { Ed25519KeypairService } from './ed25519-keypair.service';\r\nimport { SessionService } from './session.service';\r\n\r\nexport type LoginPhase = 'idle' | 'requesting-challenge' | 'signing' | 'verifying' | 'done';\r\n\r\n/**\r\n * Orchestrates the Ed25519 challenge/response admin auth flow end to end:\r\n *\r\n * GET /api/admin/auth/challenge -> { nonce }\r\n * sign(nonce) with local Ed25519 key -> signature\r\n * POST /api/admin/auth/verify -> { token, refreshToken }\r\n *\r\n * This is the lowest-level orchestrator; components should go through\r\n * AuthFacade rather than calling this directly. Exported from the package\r\n * barrel as `Ed25519AuthService` to avoid colliding with the telegram\r\n * module's `AuthService`.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class AuthService {\r\n private readonly api = inject(AuthApiService);\r\n private readonly keypair = inject(Ed25519KeypairService);\r\n private readonly session = inject(SessionService);\r\n\r\n private readonly loginPhaseSignal = signal('idle');\r\n private readonly lastErrorSignal = signal(null);\r\n\r\n readonly loginPhase = this.loginPhaseSignal.asReadonly();\r\n readonly lastError = this.lastErrorSignal.asReadonly();\r\n\r\n constructor() {\r\n this.session.onRefreshDue(() => this.refresh().subscribe());\r\n }\r\n\r\n /** Restores a persisted session on app bootstrap. Call once from an APP_INITIALIZER or root component. */\r\n restoreSession(): void {\r\n this.session.restore();\r\n }\r\n\r\n login(): Observable {\r\n this.lastErrorSignal.set(null);\r\n this.loginPhaseSignal.set('requesting-challenge');\r\n\r\n return this.api.requestChallenge().pipe(\r\n switchMap(challenge =>\r\n this.signChallenge(challenge.nonce).pipe(\r\n switchMap(({ publicKeyBase64, signature }) => {\r\n this.loginPhaseSignal.set('verifying');\r\n return this.api.verifySignature({ publicKey: publicKeyBase64, signature, nonce: challenge.nonce });\r\n })\r\n )\r\n ),\r\n tap(tokens => {\r\n this.session.activate(tokens);\r\n this.loginPhaseSignal.set('done');\r\n }),\r\n catchError(error => this.handleAuthError(error, 'invalid-signature'))\r\n );\r\n }\r\n\r\n refresh(): Observable {\r\n const refreshToken = this.session.getRefreshToken();\r\n if (!refreshToken) {\r\n this.session.markExpired();\r\n return throwError(() => this.toAuthError({ code: 'session-expired', message: 'No refresh token available.' }));\r\n }\r\n\r\n return this.api.refresh({ refreshToken }).pipe(\r\n tap(tokens => this.session.activate(tokens)),\r\n catchError(error => this.handleAuthError(error, 'session-expired', () => this.session.markExpired()))\r\n );\r\n }\r\n\r\n logout(): Observable {\r\n const refreshToken = this.session.getRefreshToken();\r\n this.session.clear();\r\n if (!refreshToken) {\r\n return new Observable(subscriber => {\r\n subscriber.next();\r\n subscriber.complete();\r\n });\r\n }\r\n return this.api.logout(refreshToken).pipe(catchError(() => throwError(() => null)));\r\n }\r\n\r\n private signChallenge(nonce: string): Observable<{ publicKeyBase64: string; signature: string }> {\r\n this.loginPhaseSignal.set('signing');\r\n return new Observable<{ publicKeyBase64: string; signature: string }>(subscriber => {\r\n this.keypair\r\n .getOrCreateKeyPair()\r\n .then(({ publicKeyBase64 }) =>\r\n this.keypair.sign(nonce).then(signature => {\r\n subscriber.next({ publicKeyBase64, signature });\r\n subscriber.complete();\r\n })\r\n )\r\n .catch(error => subscriber.error(error));\r\n });\r\n }\r\n\r\n private handleAuthError(error: unknown, fallbackCode: AuthError['code'], onError?: () => void): Observable {\r\n onError?.();\r\n return throwError(() => this.toAuthError(this.toAuthErrorShape(error, fallbackCode)));\r\n }\r\n\r\n private toAuthErrorShape(error: unknown, fallbackCode: AuthError['code']): AuthError {\r\n if (error instanceof HttpErrorResponse) {\r\n const bodyCode = (error.error as { error?: { code?: unknown } } | null)?.error?.code;\r\n const code = authErrorCodeFromBackendCode(bodyCode) ?? authErrorCodeFromStatus(error.status);\r\n return { code, message: error.message, status: error.status };\r\n }\r\n if (error instanceof Error) {\r\n return { code: fallbackCode, message: error.message };\r\n }\r\n return { code: fallbackCode, message: 'Unknown authentication error.' };\r\n }\r\n\r\n private toAuthError(error: AuthError): AuthError {\r\n this.lastErrorSignal.set(error);\r\n return error;\r\n }\r\n}\r\n","/** Roles the Ed25519 JWT `role` claim is expected to carry. Ordered highest-to-lowest privilege; PermissionService does not rely on the order, it is documentation only. */\r\nexport type AdminRole = 'Owner' | 'Administrator' | 'Editor' | 'Support' | 'ReadOnly';\r\n\r\n/**\r\n * Coarse-grained permission keys. Intentionally small and domain-agnostic -\r\n * fine-grained, per-domain permissions stay server-side; the frontend only\r\n * needs enough to hide/disable UI, never to be the source of truth for\r\n * authorization.\r\n */\r\nexport type Permission =\r\n | 'backoffice.read'\r\n | 'backoffice.write'\r\n | 'builder.read'\r\n | 'builder.write'\r\n | 'users.manage'\r\n | 'settings.manage';\r\n\r\nexport const ROLE_PERMISSIONS: Readonly> = {\r\n Owner: ['backoffice.read', 'backoffice.write', 'builder.read', 'builder.write', 'users.manage', 'settings.manage'],\r\n Administrator: ['backoffice.read', 'backoffice.write', 'builder.read', 'builder.write', 'users.manage'],\r\n Editor: ['backoffice.read', 'backoffice.write', 'builder.read', 'builder.write'],\r\n Support: ['backoffice.read'],\r\n ReadOnly: ['backoffice.read', 'builder.read']\r\n};\r\n","import { Injectable, computed, inject } from '@angular/core';\r\nimport { Permission, ROLE_PERMISSIONS } from './models/permission.model';\r\nimport { SessionService } from './session.service';\r\n\r\n/**\r\n * Derives the current admin's permission set from their JWT `role` claim.\r\n * UI-only gate (hide/disable) - the backend must independently enforce\r\n * every mutation server-side.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class PermissionService {\r\n private readonly session = inject(SessionService);\r\n\r\n readonly permissions = computed(() => {\r\n const role = this.session.role();\r\n return role ? ROLE_PERMISSIONS[role] : [];\r\n });\r\n\r\n has(permission: Permission): boolean {\r\n return this.permissions().includes(permission);\r\n }\r\n\r\n hasAny(permissions: readonly Permission[]): boolean {\r\n return permissions.some(permission => this.has(permission));\r\n }\r\n}\r\n","import { Injectable, inject } from '@angular/core';\r\nimport { Router } from '@angular/router';\r\nimport { finalize } from 'rxjs';\r\nimport { AuthService } from './auth.service';\r\nimport { PermissionService } from './permission.service';\r\nimport { SessionService } from './session.service';\r\nimport { Permission } from './models/permission.model';\r\n\r\n/**\r\n * Public surface for components/pages. Components should depend on this,\r\n * not on AuthService/SessionService/PermissionService directly, so the\r\n * orchestration details (which service owns what) can change without\r\n * touching UI code.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class AuthFacade {\r\n private readonly auth = inject(AuthService);\r\n private readonly session = inject(SessionService);\r\n private readonly permissions = inject(PermissionService);\r\n private readonly router = inject(Router);\r\n\r\n readonly isAuthenticated = this.session.isAuthenticated;\r\n readonly status = this.session.status;\r\n readonly role = this.session.role;\r\n readonly loginPhase = this.auth.loginPhase;\r\n readonly lastError = this.auth.lastError;\r\n\r\n restoreSession(): void {\r\n this.auth.restoreSession();\r\n }\r\n\r\n login(onSuccessRedirectTo?: string): void {\r\n this.auth.login().subscribe({\r\n next: () => {\r\n if (onSuccessRedirectTo) {\r\n this.router.navigateByUrl(onSuccessRedirectTo);\r\n }\r\n },\r\n error: () => {\r\n const code = this.auth.lastError()?.code ?? 'unauthorized';\r\n this.router.navigate(['/admin-login/error', code]);\r\n }\r\n });\r\n }\r\n\r\n logout(redirectTo = '/admin-login'): void {\r\n this.auth\r\n .logout()\r\n .pipe(finalize(() => this.router.navigateByUrl(redirectTo)))\r\n .subscribe({ error: () => undefined });\r\n }\r\n\r\n can(permission: Permission): boolean {\r\n return this.permissions.has(permission);\r\n }\r\n}\r\n","import { Observable } from 'rxjs';\r\n\r\n/**\r\n * Prep interfaces for a future Ed25519 challenge/response admin auth flow.\r\n * No crypto is implemented here - verification is delegated to an injectable\r\n * service so the real implementation (native WebCrypto Ed25519 support, or a\r\n * backend verification call) can be swapped in once the backend API exists,\r\n * without touching AdminAuthService or components.\r\n */\r\nexport interface Ed25519Challenge {\r\n nonce: string;\r\n timestamp: string;\r\n /** Opaque challenge payload the client must sign with its private key. */\r\n payload: string;\r\n}\r\n\r\nexport interface Ed25519SignedResponse {\r\n challenge: Ed25519Challenge;\r\n publicKey: string;\r\n signature: string;\r\n}\r\n\r\nexport interface Ed25519VerificationResult {\r\n valid: boolean;\r\n reason?: string;\r\n}\r\n\r\nexport abstract class Ed25519VerificationService {\r\n abstract requestChallenge(): Observable;\r\n abstract verify(response: Ed25519SignedResponse): Observable;\r\n}\r\n","import { Injectable } from '@angular/core';\r\nimport { Observable, throwError } from 'rxjs';\r\nimport { Ed25519Challenge, Ed25519SignedResponse, Ed25519VerificationResult, Ed25519VerificationService } from './ed25519-verification.model';\r\n\r\n/**\r\n * Default DI binding for Ed25519VerificationService until the backend ships\r\n * the real challenge/verify endpoints. Intentionally fails closed (throws)\r\n * rather than pretending to verify anything, so accidental use in a login\r\n * path is loud instead of silently accepting unsigned sessions.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class NoopEd25519VerificationService implements Ed25519VerificationService {\r\n requestChallenge(): Observable {\r\n return throwError(() => new Error('Ed25519 challenge endpoint is not yet available from the backend.'));\r\n }\r\n\r\n verify(_response: Ed25519SignedResponse): Observable {\r\n return throwError(() => new Error('Ed25519 verification endpoint is not yet available from the backend.'));\r\n }\r\n}\r\n","// @marketplaces/auth — public API barrel.\r\n// Two independent auth mechanisms, per ADR-0001 (marketplaces repo:\r\n// docs/context/adrs/ADR-0001-extract-auth-and-payment-into-shared-marketplaces-packages.md):\r\n// - telegram/ — live Telegram QR/session auth (customer + admin)\r\n// - ed25519/ — future Ed25519 challenge/response admin auth (backend not shipped yet)\r\n// Provide AUTH_API_URL (and optionally TELEGRAM_BOT_USERNAME) from the consuming app's config.\r\n\r\nexport { AUTH_API_URL, TELEGRAM_BOT_USERNAME, MARKETPLACES_AUTH_CONFIG, provideMarketplacesAuth } from './config';\nexport type { MarketplacesAuthConfig } from './config';\nexport { MARKETPLACE_DOMAIN_HEADER, AuthMarketplaceContext, normalizeMarketplaceDomain } from './marketplace-context';\nexport { MarketplacesAuthComponent } from './ui/auth.component';\nexport { MARKETPLACES_AUTH_GATEWAY, HttpMarketplacesAuthGateway } from './ui/auth.gateway';\nexport type { MarketplacesAuthGateway } from './ui/auth.gateway';\nexport type { AuthMode, AuthMethod, CredentialLogin, AuthResult, ExternalAuthStart, AuthFailure } from './ui/auth.models';\n\r\n// Telegram module\r\nexport { AuthSession, WebSessionStart, AuthStatus, AdminAuthStatus } from './telegram/models/session.model';\r\nexport { TelegramSessionApiService } from './telegram/telegram-session-api.service';\r\nexport { AuthService } from './telegram/auth.service';\r\nexport { AdminAuthService } from './telegram/admin-auth.service';\r\nexport { adminAuthGuard } from './telegram/admin-auth.guard';\r\nexport { adminAuthHeadersInterceptor } from './telegram/admin-auth-headers.interceptor';\r\n\r\n// Ed25519 module (namespaced re-exports to avoid colliding with the telegram module's AuthService)\r\nexport { AuthService as Ed25519AuthService } from './ed25519/auth.service';\r\nexport { AuthFacade } from './ed25519/auth-facade.service';\r\nexport { AuthApiService } from './ed25519/auth-api.service';\r\nexport { SessionService } from './ed25519/session.service';\r\nexport { JwtService } from './ed25519/jwt.service';\r\nexport { Ed25519KeypairService } from './ed25519/ed25519-keypair.service';\r\nexport { PermissionService } from './ed25519/permission.service';\r\nexport {\r\n Ed25519VerificationService,\r\n Ed25519Challenge,\r\n Ed25519SignedResponse,\r\n Ed25519VerificationResult\r\n} from './ed25519/ed25519-verification.model';\r\nexport { NoopEd25519VerificationService } from './ed25519/noop-ed25519-verification.service';\r\nexport {\r\n AuthChallenge,\r\n VerifySignatureRequest,\r\n AuthTokenPair,\r\n RefreshTokenRequest,\r\n JwtClaims\r\n} from './ed25519/models/auth-api.model';\r\nexport { AuthErrorCode, AuthError, authErrorCodeFromBackendCode, authErrorCodeFromStatus } from './ed25519/models/auth-error.model';\r\nexport { AdminRole, Permission, ROLE_PERMISSIONS } from './ed25519/models/permission.model';\r\nexport type { LoginPhase } from './ed25519/auth.service';\r\nexport type { SessionStatus } from './ed25519/session.service';\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":["AuthService"],"mappings":"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"} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts deleted file mode 100644 index 030fe7d..0000000 --- a/dist/index.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -export { AUTH_API_URL, TELEGRAM_BOT_USERNAME } from './config'; -export { AuthSession, WebSessionStart, AuthStatus, AdminAuthStatus } from './telegram/models/session.model'; -export { TelegramSessionApiService } from './telegram/telegram-session-api.service'; -export { AuthService } from './telegram/auth.service'; -export { AdminAuthService } from './telegram/admin-auth.service'; -export { adminAuthGuard } from './telegram/admin-auth.guard'; -export { adminAuthHeadersInterceptor } from './telegram/admin-auth-headers.interceptor'; -export { AuthService as Ed25519AuthService } from './ed25519/auth.service'; -export { AuthFacade } from './ed25519/auth-facade.service'; -export { AuthApiService } from './ed25519/auth-api.service'; -export { SessionService } from './ed25519/session.service'; -export { JwtService } from './ed25519/jwt.service'; -export { Ed25519KeypairService } from './ed25519/ed25519-keypair.service'; -export { PermissionService } from './ed25519/permission.service'; -export { Ed25519VerificationService, Ed25519Challenge, Ed25519SignedResponse, Ed25519VerificationResult } from './ed25519/ed25519-verification.model'; -export { NoopEd25519VerificationService } from './ed25519/noop-ed25519-verification.service'; -export { AuthChallenge, VerifySignatureRequest, AuthTokenPair, RefreshTokenRequest, JwtClaims } from './ed25519/models/auth-api.model'; -export { AuthErrorCode, AuthError, authErrorCodeFromBackendCode, authErrorCodeFromStatus } from './ed25519/models/auth-error.model'; -export { AdminRole, Permission, ROLE_PERMISSIONS } from './ed25519/models/permission.model'; -export type { LoginPhase } from './ed25519/auth.service'; -export type { SessionStatus } from './ed25519/session.service'; diff --git a/dist/index.js b/dist/index.js deleted file mode 100644 index 58f08cc..0000000 --- a/dist/index.js +++ /dev/null @@ -1,24 +0,0 @@ -// @marketplaces/auth — public API barrel. -// Two independent auth mechanisms, per ADR-0001 (marketplaces repo: -// docs/context/adrs/ADR-0001-extract-auth-and-payment-into-shared-marketplaces-packages.md): -// - telegram/ — live Telegram QR/session auth (customer + admin) -// - ed25519/ — future Ed25519 challenge/response admin auth (backend not shipped yet) -// Provide AUTH_API_URL (and optionally TELEGRAM_BOT_USERNAME) from the consuming app's config. -export { AUTH_API_URL, TELEGRAM_BOT_USERNAME } from './config'; -export { TelegramSessionApiService } from './telegram/telegram-session-api.service'; -export { AuthService } from './telegram/auth.service'; -export { AdminAuthService } from './telegram/admin-auth.service'; -export { adminAuthGuard } from './telegram/admin-auth.guard'; -export { adminAuthHeadersInterceptor } from './telegram/admin-auth-headers.interceptor'; -// Ed25519 module (namespaced re-exports to avoid colliding with the telegram module's AuthService) -export { AuthService as Ed25519AuthService } from './ed25519/auth.service'; -export { AuthFacade } from './ed25519/auth-facade.service'; -export { AuthApiService } from './ed25519/auth-api.service'; -export { SessionService } from './ed25519/session.service'; -export { JwtService } from './ed25519/jwt.service'; -export { Ed25519KeypairService } from './ed25519/ed25519-keypair.service'; -export { PermissionService } from './ed25519/permission.service'; -export { Ed25519VerificationService } from './ed25519/ed25519-verification.model'; -export { NoopEd25519VerificationService } from './ed25519/noop-ed25519-verification.service'; -export { authErrorCodeFromBackendCode, authErrorCodeFromStatus } from './ed25519/models/auth-error.model'; -export { ROLE_PERMISSIONS } from './ed25519/models/permission.model'; diff --git a/dist/package.json b/dist/package.json new file mode 100644 index 0000000..54efe9e --- /dev/null +++ b/dist/package.json @@ -0,0 +1,36 @@ +{ + "name": "@marketplaces/auth", + "version": "0.1.0", + "description": "Standalone Angular authentication UI and client for marketplaces projects.", + "module": "fesm2022/marketplaces-auth.mjs", + "typings": "types/marketplaces-auth.d.ts", + "files": [ + "dist" + ], + "dependencies": { + "qrcode": "^1.5.4", + "tslib": "^2.8.0" + }, + "peerDependencies": { + "@angular/core": ">=22.0.0", + "@angular/common": ">=22.0.0", + "@angular/forms": ">=22.0.0", + "@angular/router": ">=22.0.0", + "rxjs": ">=7.8.0" + }, + "publishConfig": { + "access": "restricted" + }, + "license": "UNLICENSED", + "exports": { + "./package.json": { + "default": "./package.json" + }, + ".": { + "types": "./types/marketplaces-auth.d.ts", + "default": "./fesm2022/marketplaces-auth.mjs" + } + }, + "sideEffects": false, + "type": "module" +} \ No newline at end of file diff --git a/dist/telegram/admin-auth-headers.interceptor.d.ts b/dist/telegram/admin-auth-headers.interceptor.d.ts deleted file mode 100644 index 11c87d5..0000000 --- a/dist/telegram/admin-auth-headers.interceptor.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { HttpInterceptorFn } from '@angular/common/http'; -/** - * Attaches admin session/token headers only to admin API requests. Scoped to - * admin-gated paths so it never touches customer requests and never reads - * the customer AuthService's session. - */ -export declare const adminAuthHeadersInterceptor: HttpInterceptorFn; diff --git a/dist/telegram/admin-auth-headers.interceptor.js b/dist/telegram/admin-auth-headers.interceptor.js deleted file mode 100644 index a2ade05..0000000 --- a/dist/telegram/admin-auth-headers.interceptor.js +++ /dev/null @@ -1,26 +0,0 @@ -import { inject } from '@angular/core'; -import { AdminAuthService } from './admin-auth.service'; -/** Backend paths that require an active AdminWebSessionID. Adjust to match your API surface if consuming this outside marketplaces. */ -const ADMIN_GATED_PATH_SEGMENTS = ['/admin/', '/backoffice/', '/builder/', '/media/']; -/** - * Attaches admin session/token headers only to admin API requests. Scoped to - * admin-gated paths so it never touches customer requests and never reads - * the customer AuthService's session. - */ -export const adminAuthHeadersInterceptor = (req, next) => { - const isAdminRequest = ADMIN_GATED_PATH_SEGMENTS.some(segment => req.url.includes(segment)); - if (!isAdminRequest) { - return next(req); - } - const adminAuth = inject(AdminAuthService); - const session = adminAuth.session(); - const token = adminAuth.getAdminToken(); - let headers = req.headers; - if (session?.sessionId) { - headers = headers.set('AdminWebSessionID', session.sessionId); - } - if (token) { - headers = headers.set('Authorization', `Bearer ${token}`); - } - return next(req.clone({ headers })); -}; diff --git a/dist/telegram/admin-auth.guard.d.ts b/dist/telegram/admin-auth.guard.d.ts deleted file mode 100644 index 7a5f6fe..0000000 --- a/dist/telegram/admin-auth.guard.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { CanActivateFn } from '@angular/router'; -/** Guards `/admin/**`-style routes. Never shares state with the customer auth guard/service. */ -export declare const adminAuthGuard: CanActivateFn; diff --git a/dist/telegram/admin-auth.guard.js b/dist/telegram/admin-auth.guard.js deleted file mode 100644 index 5d347dd..0000000 --- a/dist/telegram/admin-auth.guard.js +++ /dev/null @@ -1,11 +0,0 @@ -import { inject } from '@angular/core'; -import { AdminAuthService } from './admin-auth.service'; -/** Guards `/admin/**`-style routes. Never shares state with the customer auth guard/service. */ -export const adminAuthGuard = () => { - const adminAuth = inject(AdminAuthService); - if (adminAuth.isAuthenticated()) { - return true; - } - adminAuth.requestLogin(); - return false; -}; diff --git a/dist/telegram/admin-auth.service.d.ts b/dist/telegram/admin-auth.service.d.ts deleted file mode 100644 index d9361e7..0000000 --- a/dist/telegram/admin-auth.service.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Observable } from 'rxjs'; -import { AdminAuthStatus, AuthSession, WebSessionStart } from './models/session.model'; -export declare class AdminAuthService { - private readonly api; - private readonly sessionSignal; - private readonly statusSignal; - private readonly showLoginSignal; - readonly session: import("@angular/core").Signal; - readonly status: import("@angular/core").Signal; - readonly isAuthenticated: import("@angular/core").Signal; - readonly showLoginDialog: import("@angular/core").Signal; - readonly displayName: import("@angular/core").Signal; - private sessionCheckTimer?; - constructor(); - checkSession(): void; - /** Check session without mutating internal state beyond activating on success (used for polling). */ - checkSessionOnce(webSessionID?: string | null): Observable; - /** Create a backend web session - identical call to the customer login (TelegramSessionApiService.createSession). */ - createWebSession(): Observable; - getAdminAppLoginUrl(webSessionID: string): string; - onLoginComplete(): void; - requestLogin(): void; - /** - * Dev-only shortcut for local testing without a reachable Telegram/session - * backend: fabricates a local session and activates it directly, skipping - * the QR flow entirely. No-ops in production builds (checked via Angular's - * isDevMode() at runtime, not just build-time, so it is safe even if this - * code ships). Never call this from anywhere reachable in a production build. - */ - devBypassLogin(): void; - hideLogin(): void; - logout(): void; - /** JWT pair storage, reserved for once the backend issues admin access/refresh tokens. Unused until then. */ - getAdminToken(): string | null; - setAdminTokens(token: string, refreshToken: string): void; - clearAdminTokens(): void; - private activateSession; - private clearAuthState; - private scheduleSessionRefresh; - private clearSessionRefresh; - private getStoredAdminSessionID; - private setStoredAdminSessionID; - private clearStoredAdminSessionID; -} diff --git a/dist/telegram/admin-auth.service.js b/dist/telegram/admin-auth.service.js deleted file mode 100644 index 6953a16..0000000 --- a/dist/telegram/admin-auth.service.js +++ /dev/null @@ -1,188 +0,0 @@ -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -import { Injectable, signal, computed, inject, isDevMode } from '@angular/core'; -import { tap } from 'rxjs'; -import { TelegramSessionApiService } from './telegram-session-api.service'; -/** - * Admin login uses the exact same Telegram QR/session API as the customer - * login (TelegramSessionApiService) - there is no separate admin backend - * endpoint, and none should be invented client-side. Only the *storage* is - * kept separate from AuthService, so an admin QR scan never authenticates - * the customer session or vice versa: distinct cookie name, distinct - * signals, distinct guard/interceptor. - * - * Since the session API itself has no concept of "admin", the frontend - * cannot tell an admin Telegram session from a regular one. Actual admin - * authorization must be enforced server-side when admin API calls are made - * with the resulting session id - the frontend only decides where to - * *store* the result. - */ -const ADMIN_SESSION_COOKIE = 'adminSessionID'; -const ADMIN_TOKEN_STORAGE_KEY = 'adminToken'; -const ADMIN_REFRESH_STORAGE_KEY = 'adminRefreshToken'; -const ADMIN_SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60; -let AdminAuthService = class AdminAuthService { - constructor() { - this.api = inject(TelegramSessionApiService); - this.sessionSignal = signal(null); - this.statusSignal = signal('unknown'); - this.showLoginSignal = signal(false); - this.session = this.sessionSignal.asReadonly(); - this.status = this.statusSignal.asReadonly(); - this.isAuthenticated = computed(() => this.statusSignal() === 'authenticated'); - this.showLoginDialog = this.showLoginSignal.asReadonly(); - this.displayName = computed(() => this.sessionSignal()?.displayName ?? null); - this.checkSession(); - } - checkSession() { - const webSessionID = this.getStoredAdminSessionID(); - if (!webSessionID) { - this.clearAuthState('unauthenticated'); - return; - } - this.statusSignal.set('checking'); - this.checkSessionOnce(webSessionID).subscribe(session => { - if (!session?.active) { - this.clearAuthState('unauthenticated'); - } - }); - } - /** Check session without mutating internal state beyond activating on success (used for polling). */ - checkSessionOnce(webSessionID = this.getStoredAdminSessionID()) { - return this.api.checkSessionOnce(webSessionID).pipe(tap(session => { - if (session?.active) { - this.activateSession(session); - } - })); - } - /** Create a backend web session - identical call to the customer login (TelegramSessionApiService.createSession). */ - createWebSession() { - return this.api.createSession(); - } - getAdminAppLoginUrl(webSessionID) { - return this.api.getBotAppLoginUrl(webSessionID); - } - onLoginComplete() { - this.hideLogin(); - if (!this.isAuthenticated()) { - this.checkSession(); - } - } - requestLogin() { - this.showLoginSignal.set(true); - } - /** - * Dev-only shortcut for local testing without a reachable Telegram/session - * backend: fabricates a local session and activates it directly, skipping - * the QR flow entirely. No-ops in production builds (checked via Angular's - * isDevMode() at runtime, not just build-time, so it is safe even if this - * code ships). Never call this from anywhere reachable in a production build. - */ - devBypassLogin() { - if (!isDevMode()) { - return; - } - this.hideLogin(); - this.activateSession({ - sessionId: `dev-bypass-${Date.now()}`, - userId: 0, - username: 'dev-admin', - displayName: 'Dev Admin (local bypass)', - active: true, - expires: new Date(Date.now() + 60 * 60 * 1000).toISOString(), - }); - } - hideLogin() { - this.showLoginSignal.set(false); - } - logout() { - const webSessionID = this.sessionSignal()?.sessionId || this.getStoredAdminSessionID(); - if (!webSessionID) { - this.clearAuthState('unauthenticated'); - return; - } - this.api.logout(webSessionID).subscribe(() => this.clearAuthState('unauthenticated')); - } - /** JWT pair storage, reserved for once the backend issues admin access/refresh tokens. Unused until then. */ - getAdminToken() { - return typeof localStorage === 'undefined' ? null : localStorage.getItem(ADMIN_TOKEN_STORAGE_KEY); - } - setAdminTokens(token, refreshToken) { - if (typeof localStorage === 'undefined') { - return; - } - localStorage.setItem(ADMIN_TOKEN_STORAGE_KEY, token); - localStorage.setItem(ADMIN_REFRESH_STORAGE_KEY, refreshToken); - } - clearAdminTokens() { - if (typeof localStorage === 'undefined') { - return; - } - localStorage.removeItem(ADMIN_TOKEN_STORAGE_KEY); - localStorage.removeItem(ADMIN_REFRESH_STORAGE_KEY); - } - activateSession(session) { - this.sessionSignal.set(session); - this.statusSignal.set('authenticated'); - this.setStoredAdminSessionID(session.sessionId); - this.scheduleSessionRefresh(session.expires); - } - clearAuthState(status) { - this.sessionSignal.set(null); - this.statusSignal.set(status); - this.clearStoredAdminSessionID(); - this.clearAdminTokens(); - this.clearSessionRefresh(); - } - scheduleSessionRefresh(expiresAt) { - this.clearSessionRefresh(); - const expiresMs = new Date(expiresAt).getTime(); - const nowMs = Date.now(); - const refreshIn = Number.isFinite(expiresMs) - ? Math.max(expiresMs - nowMs - 60_000, 30_000) - : ADMIN_SESSION_COOKIE_MAX_AGE_SECONDS * 1000; - this.sessionCheckTimer = setTimeout(() => this.checkSession(), refreshIn); - } - clearSessionRefresh() { - if (this.sessionCheckTimer) { - clearTimeout(this.sessionCheckTimer); - this.sessionCheckTimer = undefined; - } - } - getStoredAdminSessionID() { - if (typeof document === 'undefined') { - return null; - } - const cookie = document.cookie.split('; ').find(row => row.startsWith(`${ADMIN_SESSION_COOKIE}=`)); - if (!cookie) { - return null; - } - try { - return decodeURIComponent(cookie.substring(ADMIN_SESSION_COOKIE.length + 1)); - } - catch { - return null; - } - } - setStoredAdminSessionID(webSessionID) { - if (typeof document === 'undefined') { - return; - } - const secure = typeof window !== 'undefined' && window.location.protocol === 'https:' ? '; Secure' : ''; - document.cookie = `${ADMIN_SESSION_COOKIE}=${encodeURIComponent(webSessionID)}; Max-Age=${ADMIN_SESSION_COOKIE_MAX_AGE_SECONDS}; Path=/; SameSite=Strict${secure}`; - } - clearStoredAdminSessionID() { - if (typeof document === 'undefined') { - return; - } - document.cookie = `${ADMIN_SESSION_COOKIE}=; Max-Age=0; Path=/; SameSite=Strict`; - } -}; -AdminAuthService = __decorate([ - Injectable({ providedIn: 'root' }) -], AdminAuthService); -export { AdminAuthService }; diff --git a/dist/telegram/auth.service.d.ts b/dist/telegram/auth.service.d.ts deleted file mode 100644 index cfefaf6..0000000 --- a/dist/telegram/auth.service.d.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Observable } from 'rxjs'; -import { AuthSession, AuthStatus, WebSessionStart } from './models/session.model'; -/** Customer-facing Telegram QR/session auth. Distinct storage/state from AdminAuthService by design. */ -export declare class AuthService { - private readonly api; - private sessionSignal; - private statusSignal; - private showLoginSignal; - /** Current auth session */ - readonly session: import("@angular/core").Signal; - /** Current auth status */ - readonly status: import("@angular/core").Signal; - /** Whether user is fully authenticated */ - readonly isAuthenticated: import("@angular/core").Signal; - /** Whether to show login dialog */ - readonly showLoginDialog: import("@angular/core").Signal; - /** Display name of authenticated user */ - readonly displayName: import("@angular/core").Signal; - private sessionCheckTimer?; - constructor(); - /** Check the current webSessionID cookie against the auth backend. */ - checkSession(): void; - /** Check session without updating internal state beyond activating on success (used for polling). */ - checkSessionOnce(webSessionID?: string | null): Observable; - /** - * Called after user completes Telegram login. - */ - onTelegramLoginComplete(): void; - /** Generate the Telegram login URL for bot-based auth */ - getTelegramLoginUrl(webSessionID: string): string; - /** Generate a Telegram app deep link for mobile login without opening a browser tab. */ - getTelegramAppLoginUrl(webSessionID: string): string; - /** Create a backend web session and return the Telegram start link for it. */ - createWebSession(): Observable; - /** Show login dialog (called when user tries to pay without being logged in) */ - requestLogin(): void; - /** Hide login dialog */ - hideLogin(): void; - /** Logout — clears session on backend and locally */ - logout(): void; - private activateSession; - private clearAuthState; - /** Schedule a session re-check before it expires */ - private scheduleSessionRefresh; - private clearSessionRefresh; - private getStoredWebSessionID; - private setStoredWebSessionID; - private clearStoredWebSessionID; -} diff --git a/dist/telegram/auth.service.js b/dist/telegram/auth.service.js deleted file mode 100644 index d518aa4..0000000 --- a/dist/telegram/auth.service.js +++ /dev/null @@ -1,161 +0,0 @@ -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -import { Injectable, signal, computed, inject } from '@angular/core'; -import { tap } from 'rxjs'; -import { TelegramSessionApiService } from './telegram-session-api.service'; -const WEB_SESSION_COOKIE = 'webSessionID'; -const WEB_SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60; -/** Customer-facing Telegram QR/session auth. Distinct storage/state from AdminAuthService by design. */ -let AuthService = class AuthService { - constructor() { - this.api = inject(TelegramSessionApiService); - this.sessionSignal = signal(null); - this.statusSignal = signal('unknown'); - this.showLoginSignal = signal(false); - /** Current auth session */ - this.session = this.sessionSignal.asReadonly(); - /** Current auth status */ - this.status = this.statusSignal.asReadonly(); - /** Whether user is fully authenticated */ - this.isAuthenticated = computed(() => this.statusSignal() === 'authenticated'); - /** Whether to show login dialog */ - this.showLoginDialog = this.showLoginSignal.asReadonly(); - /** Display name of authenticated user */ - this.displayName = computed(() => this.sessionSignal()?.displayName ?? null); - // On init, check existing session via cookie - this.checkSession(); - } - /** Check the current webSessionID cookie against the auth backend. */ - checkSession() { - const webSessionID = this.getStoredWebSessionID(); - if (!webSessionID) { - this.clearAuthState('unauthenticated'); - return; - } - this.statusSignal.set('checking'); - this.checkSessionOnce(webSessionID).subscribe(session => { - if (!session?.active) { - this.clearAuthState('unauthenticated'); - } - }); - } - /** Check session without updating internal state beyond activating on success (used for polling). */ - checkSessionOnce(webSessionID = this.getStoredWebSessionID()) { - return this.api.checkSessionOnce(webSessionID).pipe(tap(session => { - if (session?.active) { - this.activateSession(session); - } - })); - } - /** - * Called after user completes Telegram login. - */ - onTelegramLoginComplete() { - this.hideLogin(); - if (!this.isAuthenticated()) { - this.checkSession(); - } - } - /** Generate the Telegram login URL for bot-based auth */ - getTelegramLoginUrl(webSessionID) { - return this.api.getBotLoginUrl(webSessionID); - } - /** Generate a Telegram app deep link for mobile login without opening a browser tab. */ - getTelegramAppLoginUrl(webSessionID) { - return this.api.getBotAppLoginUrl(webSessionID); - } - /** Create a backend web session and return the Telegram start link for it. */ - createWebSession() { - return this.api.createSession(); - } - /** Show login dialog (called when user tries to pay without being logged in) */ - requestLogin() { - this.showLoginSignal.set(true); - } - /** Hide login dialog */ - hideLogin() { - this.showLoginSignal.set(false); - } - /** Logout — clears session on backend and locally */ - logout() { - const webSessionID = this.sessionSignal()?.sessionId || this.getStoredWebSessionID(); - if (!webSessionID) { - this.clearAuthState('unauthenticated'); - return; - } - this.api.logout(webSessionID).subscribe(() => { - this.clearAuthState('unauthenticated'); - }); - } - activateSession(session) { - this.sessionSignal.set(session); - this.statusSignal.set('authenticated'); - this.setStoredWebSessionID(session.sessionId); - this.scheduleSessionRefresh(session.expires); - } - clearAuthState(status) { - this.sessionSignal.set(null); - this.statusSignal.set(status); - this.clearStoredWebSessionID(); - this.clearSessionRefresh(); - } - /** Schedule a session re-check before it expires */ - scheduleSessionRefresh(expiresAt) { - this.clearSessionRefresh(); - const expiresMs = new Date(expiresAt).getTime(); - const nowMs = Date.now(); - // Re-check 60 seconds before expiry, minimum 30s from now - const refreshIn = Number.isFinite(expiresMs) - ? Math.max(expiresMs - nowMs - 60_000, 30_000) - : WEB_SESSION_COOKIE_MAX_AGE_SECONDS * 1000; - this.sessionCheckTimer = setTimeout(() => { - this.checkSession(); - }, refreshIn); - } - clearSessionRefresh() { - if (this.sessionCheckTimer) { - clearTimeout(this.sessionCheckTimer); - this.sessionCheckTimer = undefined; - } - } - getStoredWebSessionID() { - if (typeof document === 'undefined') { - return null; - } - const cookie = document.cookie - .split('; ') - .find(row => row.startsWith(`${WEB_SESSION_COOKIE}=`)); - if (!cookie) { - return null; - } - try { - return decodeURIComponent(cookie.substring(WEB_SESSION_COOKIE.length + 1)); - } - catch { - return null; - } - } - setStoredWebSessionID(webSessionID) { - if (typeof document === 'undefined') { - return; - } - const secure = typeof window !== 'undefined' && window.location.protocol === 'https:' ? '; Secure' : ''; - document.cookie = `${WEB_SESSION_COOKIE}=${encodeURIComponent(webSessionID)}; Max-Age=${WEB_SESSION_COOKIE_MAX_AGE_SECONDS}; Path=/; SameSite=Lax${secure}`; - } - clearStoredWebSessionID() { - if (typeof document === 'undefined') { - return; - } - document.cookie = `${WEB_SESSION_COOKIE}=; Max-Age=0; Path=/; SameSite=Lax`; - } -}; -AuthService = __decorate([ - Injectable({ - providedIn: 'root' - }) -], AuthService); -export { AuthService }; diff --git a/dist/telegram/models/session.model.d.ts b/dist/telegram/models/session.model.d.ts deleted file mode 100644 index fa97ba7..0000000 --- a/dist/telegram/models/session.model.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface AuthSession { - sessionId: string; - userId: number | null; - username: string | null; - displayName: string; - active: boolean; - expires: string; -} -export interface WebSessionStart { - webSessionID: string; - url: string; -} -export type AuthStatus = 'unknown' | 'checking' | 'authenticated' | 'expired' | 'unauthenticated'; -export type AdminAuthStatus = 'unknown' | 'checking' | 'authenticated' | 'expired' | 'unauthenticated'; diff --git a/dist/telegram/models/session.model.js b/dist/telegram/models/session.model.js deleted file mode 100644 index cb0ff5c..0000000 --- a/dist/telegram/models/session.model.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/dist/telegram/telegram-session-api.service.d.ts b/dist/telegram/telegram-session-api.service.d.ts deleted file mode 100644 index 5ea8650..0000000 --- a/dist/telegram/telegram-session-api.service.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Observable } from 'rxjs'; -import { AuthSession, WebSessionStart } from './models/session.model'; -/** - * The one Telegram QR/session API (`{authApiUrl}/users/sessions`). Customer - * login (AuthService) and admin login (AdminAuthService) both call this same - * service against this same endpoint - there is no separate admin backend. - * This class only does the HTTP call + response normalization; it holds no - * session state and writes no cookies, so each caller manages its own - * storage/signals independently on top of it. - */ -export declare class TelegramSessionApiService { - private readonly http; - private readonly authApiUrl; - private readonly telegramBotUsername; - createSession(): Observable; - checkSessionOnce(webSessionID: string | null): Observable; - logout(webSessionID: string): Observable; - getBotLoginUrl(webSessionID: string): string; - getBotAppLoginUrl(webSessionID: string): string; - private getBotUsername; - private normalizeWebSession; - private extractSessionId; - private readFirst; - private readString; - private readNumber; - private asRecord; - private isActiveStatus; -} diff --git a/dist/telegram/telegram-session-api.service.js b/dist/telegram/telegram-session-api.service.js deleted file mode 100644 index d3149ff..0000000 --- a/dist/telegram/telegram-session-api.service.js +++ /dev/null @@ -1,137 +0,0 @@ -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { of, catchError, map } from 'rxjs'; -import { AUTH_API_URL, TELEGRAM_BOT_USERNAME } from '../config'; -import { generateGuid } from '../util/guid.util'; -const SESSION_MAX_AGE_SECONDS = 60 * 60; -const DEFAULT_TELEGRAM_BOT_USERNAME = 'DexarSupport_bot'; -/** - * The one Telegram QR/session API (`{authApiUrl}/users/sessions`). Customer - * login (AuthService) and admin login (AdminAuthService) both call this same - * service against this same endpoint - there is no separate admin backend. - * This class only does the HTTP call + response normalization; it holds no - * session state and writes no cookies, so each caller manages its own - * storage/signals independently on top of it. - */ -let TelegramSessionApiService = class TelegramSessionApiService { - constructor() { - this.http = inject(HttpClient); - this.authApiUrl = inject(AUTH_API_URL); - this.telegramBotUsername = inject(TELEGRAM_BOT_USERNAME, { optional: true }); - } - createSession() { - const webSessionID = generateGuid(); - return this.http.post(`${this.authApiUrl}/users/sessions`, { webSessionID }, { headers: { WebSessionID: webSessionID } }).pipe(map(response => { - const responseWebSessionID = this.extractSessionId(response, webSessionID); - return { - webSessionID: responseWebSessionID, - url: this.getBotLoginUrl(responseWebSessionID), - }; - })); - } - checkSessionOnce(webSessionID) { - if (!webSessionID) { - return of(null); - } - return this.http.get(`${this.authApiUrl}/users/sessions/${encodeURIComponent(webSessionID)}`).pipe(map(response => this.normalizeWebSession(response, webSessionID)), catchError(() => of(null))); - } - logout(webSessionID) { - return this.http.delete(`${this.authApiUrl}/users/sessions/${encodeURIComponent(webSessionID)}`, { - headers: { WebSessionID: webSessionID } - }).pipe(catchError(() => of(null))); - } - getBotLoginUrl(webSessionID) { - return `https://t.me/${this.getBotUsername()}?start=${encodeURIComponent(webSessionID)}`; - } - getBotAppLoginUrl(webSessionID) { - return `tg://resolve?domain=${encodeURIComponent(this.getBotUsername())}&start=${encodeURIComponent(webSessionID)}`; - } - getBotUsername() { - return this.telegramBotUsername || DEFAULT_TELEGRAM_BOT_USERNAME; - } - normalizeWebSession(response, fallbackSessionId) { - if (!response) { - return null; - } - const user = this.asRecord(this.readFirst(response, ['user', 'User', 'telegramUser', 'TelegramUser'])) ?? response; - const status = this.readFirst(response, [ - 'status', 'Status', 'active', 'Active', 'loggedIn', 'LoggedIn', - 'isLoggedIn', 'IsLoggedIn', 'authenticated', 'Authenticated' - ]); - const active = this.isActiveStatus(status); - const sessionId = this.extractSessionId(response, fallbackSessionId); - const username = this.readString(this.readFirst(user, ['username', 'Username'])) - ?? this.readString(this.readFirst(response, ['username', 'Username'])); - const firstName = this.readString(this.readFirst(user, ['firstName', 'first_name', 'FirstName', 'First_name'])); - const lastName = this.readString(this.readFirst(user, ['lastName', 'last_name', 'LastName', 'Last_name'])); - const fullName = [firstName, lastName].filter(Boolean).join(' '); - const explicitDisplayName = this.readString(this.readFirst(response, ['displayName', 'DisplayName', 'name', 'Name'])) - ?? this.readString(this.readFirst(user, ['displayName', 'DisplayName', 'name', 'Name'])); - const displayName = explicitDisplayName ?? username ?? (fullName || 'Telegram User'); - const telegramUserId = this.readNumber(this.readFirst(user, ['userId', 'telegramUserId', 'telegramUserID', 'TelegramUserID', 'id', 'ID'])) - ?? this.readNumber(this.readFirst(response, ['userId', 'telegramUserId', 'telegramUserID', 'TelegramUserID', 'userID', 'UserID', 'UserId'])) - ?? null; - const expiresAt = this.readString(this.readFirst(response, ['expiresAt', 'ExpiresAt', 'expires', 'Expires'])) - ?? new Date(Date.now() + SESSION_MAX_AGE_SECONDS * 1000).toISOString(); - return { sessionId, userId: telegramUserId, username, displayName, active, expires: expiresAt }; - } - extractSessionId(response, fallbackSessionId) { - if (!response) { - return fallbackSessionId; - } - return this.readString(this.readFirst(response, [ - 'webSessionID', 'WebSessionID', 'webSessionId', 'sessionID', 'SessionID', 'sessionId', 'id', 'ID' - ])) ?? fallbackSessionId; - } - readFirst(source, keys) { - for (const key of keys) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - return source[key]; - } - } - return undefined; - } - readString(value) { - if (typeof value === 'string' && value.trim()) { - return value; - } - if (typeof value === 'number' || typeof value === 'bigint') { - return value.toString(); - } - return null; - } - readNumber(value) { - if (typeof value === 'number' && Number.isFinite(value)) { - return value; - } - if (typeof value === 'string') { - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : null; - } - return null; - } - asRecord(value) { - return value !== null && typeof value === 'object' && !Array.isArray(value) - ? value - : null; - } - isActiveStatus(status) { - if (status === true || status === 1) { - return true; - } - if (typeof status !== 'string') { - return false; - } - return ['true', '1', 'active', 'authenticated', 'confirmed', 'success', 'logged_in'].includes(status.toLowerCase()); - } -}; -TelegramSessionApiService = __decorate([ - Injectable({ providedIn: 'root' }) -], TelegramSessionApiService); -export { TelegramSessionApiService }; diff --git a/dist/types/marketplaces-auth.d.ts b/dist/types/marketplaces-auth.d.ts new file mode 100644 index 0000000..9abbbaf --- /dev/null +++ b/dist/types/marketplaces-auth.d.ts @@ -0,0 +1,545 @@ +import * as _angular_core from '@angular/core'; +import { InjectionToken, EnvironmentProviders } from '@angular/core'; +import { HttpHeaders, HttpInterceptorFn } from '@angular/common/http'; +import * as _angular_forms_signals from '@angular/forms/signals'; +import { Observable } from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import * as _marketplaces_auth from '@marketplaces/auth'; + +interface MarketplacesAuthConfig { + /** Central auth service URL. It is not the tenant API URL. */ + apiUrl: string; + /** Override only for SSR/custom-domain integrations. Browser default is location.hostname. */ + marketplaceDomain?: string | (() => string); + telegramBotUsername?: string; + credentialsPath?: string; + yandexStartPath?: string; + yandexSessionPath?: string; + pollIntervalMs?: number; +} +/** Base URL for the auth backend, e.g. `https://api.example.com`. Provide from the consuming app's environment config. */ +declare const AUTH_API_URL: InjectionToken; +/** Telegram bot username used to build QR/deep-link login URLs. Optional — falls back to a default if not provided. */ +declare const TELEGRAM_BOT_USERNAME: InjectionToken; +declare const MARKETPLACES_AUTH_CONFIG: InjectionToken; +declare function provideMarketplacesAuth(config: MarketplacesAuthConfig): EnvironmentProviders; + +declare const MARKETPLACE_DOMAIN_HEADER = "X-Marketplace-Domain"; +declare function normalizeMarketplaceDomain(domain: string): string; +declare class AuthMarketplaceContext { + private readonly config; + domain(): string; + headers(extra?: Record): HttpHeaders; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; + static ɵprov: _angular_core.ɵɵInjectableDeclaration; +} + +interface AuthSession { + sessionId: string; + userId: number | null; + username: string | null; + displayName: string; + active: boolean; + expires: string; +} +interface WebSessionStart { + webSessionID: string; + url: string; +} +type AuthStatus = 'unknown' | 'checking' | 'authenticated' | 'expired' | 'unauthenticated'; +type AdminAuthStatus = 'unknown' | 'checking' | 'authenticated' | 'expired' | 'unauthenticated'; + +type AuthMode = 'customer' | 'admin'; +type AuthMethod = 'qr' | 'credentials' | 'yandex'; +interface CredentialLogin { + login: string; + password: string; +} +interface AuthResult { + method: AuthMethod; + mode: AuthMode; + session: AuthSession; + accessToken?: string; + refreshToken?: string; +} +interface ExternalAuthStart { + attemptId: string; + authorizationUrl: string; +} +interface AuthFailure { + method: AuthMethod; + code: 'configuration' | 'invalid_credentials' | 'backend' | 'popup_blocked' | 'expired'; + message: string; + cause?: unknown; +} + +declare class MarketplacesAuthComponent { + readonly qr: _angular_core.InputSignalWithTransform; + readonly credentials: _angular_core.InputSignalWithTransform; + readonly yandex: _angular_core.InputSignalWithTransform; + readonly mode: _angular_core.InputSignal; + readonly title: _angular_core.InputSignal; + readonly authenticated: _angular_core.OutputEmitterRef; + readonly authError: _angular_core.OutputEmitterRef; + readonly cancelled: _angular_core.OutputEmitterRef; + readonly method: _angular_core.WritableSignal; + readonly busy: _angular_core.WritableSignal; + readonly error: _angular_core.WritableSignal; + readonly qrImage: _angular_core.WritableSignal; + readonly externalUrl: _angular_core.WritableSignal; + private readonly credentialsModel; + readonly credentialsForm: _angular_forms_signals.FieldTree<{ + login: string; + password: string; + }, string | number, "writable">; + private readonly gateway; + private readonly config; + private poll?; + constructor(); + select(method: AuthMethod | null): void; + startQr(): void; + loginWithCredentials(event: Event): void; + startYandex(): void; + private pollForQr; + private prepareQr; + private pollForYandex; + private begin; + private finish; + private fail; + private isFailure; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; + static ɵcmp: _angular_core.ɵɵComponentDeclaration; +} + +interface MarketplacesAuthGateway { + startQr(mode: AuthMode): Observable; + checkQr(mode: AuthMode, attemptId: string): Observable; + loginWithCredentials(mode: AuthMode, credentials: CredentialLogin): Observable; + startYandex(mode: AuthMode, returnUrl: string): Observable; + checkYandex(mode: AuthMode, attemptId: string): Observable; +} +declare const MARKETPLACES_AUTH_GATEWAY: InjectionToken; +declare class HttpMarketplacesAuthGateway implements MarketplacesAuthGateway { + private readonly http; + private readonly config; + private readonly context; + private readonly customerAuth; + private readonly adminAuth; + startQr(mode: AuthMode): Observable; + checkQr(mode: AuthMode, attemptId: string): Observable; + loginWithCredentials(mode: AuthMode, credentials: CredentialLogin): Observable; + startYandex(mode: AuthMode, returnUrl: string): Observable; + checkYandex(mode: AuthMode, attemptId: string): Observable; + private accept; + private url; + private failure; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; + static ɵprov: _angular_core.ɵɵInjectableDeclaration; +} + +/** + * The one Telegram QR/session API (`{authApiUrl}/users/sessions`). Customer + * login (AuthService) and admin login (AdminAuthService) both call this same + * service against this same endpoint - there is no separate admin backend. + * This class only does the HTTP call + response normalization; it holds no + * session state and writes no cookies, so each caller manages its own + * storage/signals independently on top of it. + */ +declare class TelegramSessionApiService { + private readonly http; + private readonly authApiUrl; + private readonly telegramBotUsername; + private readonly marketplaceContext; + createSession(): Observable; + checkSessionOnce(webSessionID: string | null): Observable; + logout(webSessionID: string): Observable; + getBotLoginUrl(webSessionID: string): string; + getBotAppLoginUrl(webSessionID: string): string; + private getBotUsername; + private normalizeWebSession; + private extractSessionId; + private readFirst; + private readString; + private readNumber; + private asRecord; + private isActiveStatus; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; + static ɵprov: _angular_core.ɵɵInjectableDeclaration; +} + +/** Customer-facing Telegram QR/session auth. Distinct storage/state from AdminAuthService by design. */ +declare class AuthService$1 { + private readonly api; + private sessionSignal; + private statusSignal; + private showLoginSignal; + /** Current auth session */ + readonly session: _angular_core.Signal; + /** Current auth status */ + readonly status: _angular_core.Signal; + /** Whether user is fully authenticated */ + readonly isAuthenticated: _angular_core.Signal; + /** Whether to show login dialog */ + readonly showLoginDialog: _angular_core.Signal; + /** Display name of authenticated user */ + readonly displayName: _angular_core.Signal; + private sessionCheckTimer?; + constructor(); + /** Check the current webSessionID cookie against the auth backend. */ + checkSession(): void; + /** Check session without updating internal state beyond activating on success (used for polling). */ + checkSessionOnce(webSessionID?: string | null): Observable; + /** + * Called after user completes Telegram login. + */ + onTelegramLoginComplete(): void; + /** Generate the Telegram login URL for bot-based auth */ + getTelegramLoginUrl(webSessionID: string): string; + /** Generate a Telegram app deep link for mobile login without opening a browser tab. */ + getTelegramAppLoginUrl(webSessionID: string): string; + /** Create a backend web session and return the Telegram start link for it. */ + createWebSession(): Observable; + /** Show login dialog (called when user tries to pay without being logged in) */ + requestLogin(): void; + /** Hide login dialog */ + hideLogin(): void; + /** Logout — clears session on backend and locally */ + logout(): void; + /** Accept a session returned by credentials or an external provider. */ + acceptSession(session: AuthSession): void; + private activateSession; + private clearAuthState; + /** Schedule a session re-check before it expires */ + private scheduleSessionRefresh; + private clearSessionRefresh; + private getStoredWebSessionID; + private setStoredWebSessionID; + private clearStoredWebSessionID; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; + static ɵprov: _angular_core.ɵɵInjectableDeclaration; +} + +declare class AdminAuthService { + private readonly api; + private readonly sessionSignal; + private readonly statusSignal; + private readonly showLoginSignal; + readonly session: _angular_core.Signal; + readonly status: _angular_core.Signal; + readonly isAuthenticated: _angular_core.Signal; + readonly showLoginDialog: _angular_core.Signal; + readonly displayName: _angular_core.Signal; + private sessionCheckTimer?; + constructor(); + checkSession(): void; + /** Check session without mutating internal state beyond activating on success (used for polling). */ + checkSessionOnce(webSessionID?: string | null): Observable; + /** Create a backend web session - identical call to the customer login (TelegramSessionApiService.createSession). */ + createWebSession(): Observable; + getAdminAppLoginUrl(webSessionID: string): string; + onLoginComplete(): void; + requestLogin(): void; + /** + * Dev-only shortcut for local testing without a reachable Telegram/session + * backend: fabricates a local session and activates it directly, skipping + * the QR flow entirely. No-ops in production builds (checked via Angular's + * isDevMode() at runtime, not just build-time, so it is safe even if this + * code ships). Never call this from anywhere reachable in a production build. + */ + devBypassLogin(): void; + hideLogin(): void; + logout(): void; + /** Accept a session/tokens returned by credentials or an external provider. */ + acceptSession(session: AuthSession, token?: string, refreshToken?: string): void; + /** JWT pair storage, reserved for once the backend issues admin access/refresh tokens. Unused until then. */ + getAdminToken(): string | null; + setAdminTokens(token: string, refreshToken: string): void; + clearAdminTokens(): void; + private activateSession; + private clearAuthState; + private scheduleSessionRefresh; + private clearSessionRefresh; + private getStoredAdminSessionID; + private setStoredAdminSessionID; + private clearStoredAdminSessionID; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; + static ɵprov: _angular_core.ɵɵInjectableDeclaration; +} + +/** Guards `/admin/**`-style routes. Never shares state with the customer auth guard/service. */ +declare const adminAuthGuard: CanActivateFn; + +/** + * Attaches admin session/token headers only to admin API requests. Scoped to + * admin-gated paths so it never touches customer requests and never reads + * the customer AuthService's session. + */ +declare const adminAuthHeadersInterceptor: HttpInterceptorFn; + +/** Roles the Ed25519 JWT `role` claim is expected to carry. Ordered highest-to-lowest privilege; PermissionService does not rely on the order, it is documentation only. */ +type AdminRole = 'Owner' | 'Administrator' | 'Editor' | 'Support' | 'ReadOnly'; +/** + * Coarse-grained permission keys. Intentionally small and domain-agnostic - + * fine-grained, per-domain permissions stay server-side; the frontend only + * needs enough to hide/disable UI, never to be the source of truth for + * authorization. + */ +type Permission = 'backoffice.read' | 'backoffice.write' | 'builder.read' | 'builder.write' | 'users.manage' | 'settings.manage'; +declare const ROLE_PERMISSIONS: Readonly>; + +/** Wire contracts for the Ed25519 challenge/response admin auth flow. */ +interface AuthChallenge { + nonce: string; + /** ISO 8601 issue time of the challenge. */ + issuedAt: string; + /** ISO 8601 - challenge must be used before this or the backend rejects it. */ + expiresAt: string; +} +interface VerifySignatureRequest { + publicKey: string; + signature: string; + nonce: string; +} +interface AuthTokenPair { + token: string; + refreshToken: string; +} +interface RefreshTokenRequest { + refreshToken: string; +} +/** + * Claims expected in the JWT `token`. Decoded client-side for display/UX + * only (role-gating UI, expiry countdown) - the frontend never treats this + * as proof of authorization; every admin request is still re-checked + * server-side. + */ +interface JwtClaims { + sub: string; + role: AdminRole; + /** Issued-at, seconds since epoch (standard `iat` claim). */ + iat: number; + /** Expiry, seconds since epoch (standard `exp` claim). */ + exp: number; + publicKey: string; +} + +/** + * Error codes the Ed25519 admin auth flow can surface to the UI. Each maps to + * a dedicated screen rather than a generic toast, because the recovery + * action differs per code (re-login vs. retry vs. wait). + */ +type AuthErrorCode = 'session-expired' | 'invalid-signature' | 'unauthorized' | 'forbidden' | 'backend-unavailable'; +interface AuthError { + code: AuthErrorCode; + message: string; + /** HTTP status that produced this error, when known (absent for client-side errors, e.g. no Ed25519 support). */ + status?: number; +} +declare function authErrorCodeFromBackendCode(code: unknown): AuthErrorCode | undefined; +/** Maps a backend HTTP status to the AuthErrorCode screen it should route to. */ +declare function authErrorCodeFromStatus(status: number): AuthErrorCode; + +type LoginPhase = 'idle' | 'requesting-challenge' | 'signing' | 'verifying' | 'done'; +/** + * Orchestrates the Ed25519 challenge/response admin auth flow end to end: + * + * GET /api/admin/auth/challenge -> { nonce } + * sign(nonce) with local Ed25519 key -> signature + * POST /api/admin/auth/verify -> { token, refreshToken } + * + * This is the lowest-level orchestrator; components should go through + * AuthFacade rather than calling this directly. Exported from the package + * barrel as `Ed25519AuthService` to avoid colliding with the telegram + * module's `AuthService`. + */ +declare class AuthService { + private readonly api; + private readonly keypair; + private readonly session; + private readonly loginPhaseSignal; + private readonly lastErrorSignal; + readonly loginPhase: _angular_core.Signal; + readonly lastError: _angular_core.Signal; + constructor(); + /** Restores a persisted session on app bootstrap. Call once from an APP_INITIALIZER or root component. */ + restoreSession(): void; + login(): Observable; + refresh(): Observable; + logout(): Observable; + private signChallenge; + private handleAuthError; + private toAuthErrorShape; + private toAuthError; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; + static ɵprov: _angular_core.ɵɵInjectableDeclaration; +} + +/** + * Public surface for components/pages. Components should depend on this, + * not on AuthService/SessionService/PermissionService directly, so the + * orchestration details (which service owns what) can change without + * touching UI code. + */ +declare class AuthFacade { + private readonly auth; + private readonly session; + private readonly permissions; + private readonly router; + readonly isAuthenticated: _angular_core.Signal; + readonly status: _angular_core.Signal<_marketplaces_auth.SessionStatus>; + readonly role: _angular_core.Signal<_marketplaces_auth.AdminRole | null>; + readonly loginPhase: _angular_core.Signal<_marketplaces_auth.LoginPhase>; + readonly lastError: _angular_core.Signal<_marketplaces_auth.AuthError | null>; + restoreSession(): void; + login(onSuccessRedirectTo?: string): void; + logout(redirectTo?: string): void; + can(permission: Permission): boolean; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; + static ɵprov: _angular_core.ɵɵInjectableDeclaration; +} + +/** + * Thin HTTP client for the Ed25519 admin auth endpoints. These endpoints may + * not exist on every backend yet - calling them before the backend ships + * 404s or connection-errors, which AuthService maps to the + * `backend-unavailable` error screen. No mock/fake responses are fabricated + * here; this is real HttpClient wiring against the real contract. + */ +declare class AuthApiService { + private readonly http; + private readonly baseUrl; + requestChallenge(): Observable; + verifySignature(request: VerifySignatureRequest): Observable; + refresh(request: RefreshTokenRequest): Observable; + logout(refreshToken: string): Observable; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; + static ɵprov: _angular_core.ɵɵInjectableDeclaration; +} + +type SessionStatus = 'unknown' | 'restoring' | 'authenticated' | 'unauthenticated' | 'expired'; +/** + * Holds the Ed25519-flow JWT/refresh-token pair and derived claims. Separate + * from the telegram module's AdminAuthService (Telegram-session state) by + * design - the two auth mechanisms are not merged until both ship on the + * same backend and a migration decision is made. + */ +declare class SessionService { + private readonly jwt; + private readonly tokenSignal; + private readonly refreshTokenSignal; + private readonly claimsSignal; + private readonly statusSignal; + readonly token: _angular_core.Signal; + readonly claims: _angular_core.Signal; + readonly status: _angular_core.Signal; + readonly isAuthenticated: _angular_core.Signal; + readonly role: _angular_core.Signal<_marketplaces_auth.AdminRole | null>; + private refreshTimer?; + private refreshCallback?; + /** Called once by AuthService on init to wire up the refresh trigger without a circular DI dependency. */ + onRefreshDue(callback: () => void): void; + /** Restores session state from persisted storage. Returns true if a (possibly expired) session was found. */ + restore(): boolean; + activate(tokens: AuthTokenPair): void; + getRefreshToken(): string | null; + markExpired(): void; + clear(): void; + private scheduleRefresh; + private clearRefreshTimer; + private readStorage; + private writeStorage; + private removeStorage; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; + static ɵprov: _angular_core.ɵɵInjectableDeclaration; +} + +/** + * Client-side JWT *decoding* only - never verification. The signature is + * meaningless to check here because the frontend has no trusted key to check + * it against; verifying a JWT's signature is the backend's job on every + * request. This service exists purely so the UI can read `role`/`exp` for + * display and route-gating UX (e.g. "session expires in 4m"). + */ +declare class JwtService { + decode(token: string): JwtClaims | null; + isExpired(claims: JwtClaims, skewSeconds?: number): boolean; + private isJwtClaims; + private base64UrlDecode; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; + static ɵprov: _angular_core.ɵɵInjectableDeclaration; +} + +declare class Ed25519KeypairService { + private cached; + isSupported(): boolean; + /** Returns the device's Ed25519 keypair, generating and persisting one on first use. */ + getOrCreateKeyPair(): Promise<{ + publicKeyBase64: string; + }>; + sign(message: string): Promise; + /** Discards the local keypair (e.g. "forget this device"). A new keypair on next login requires re-registration with the backend. */ + clear(): Promise; + private generateAndPersist; + private loadFromStore; + private openDatabase; + private toBase64; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; + static ɵprov: _angular_core.ɵɵInjectableDeclaration; +} + +/** + * Derives the current admin's permission set from their JWT `role` claim. + * UI-only gate (hide/disable) - the backend must independently enforce + * every mutation server-side. + */ +declare class PermissionService { + private readonly session; + readonly permissions: _angular_core.Signal; + has(permission: Permission): boolean; + hasAny(permissions: readonly Permission[]): boolean; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; + static ɵprov: _angular_core.ɵɵInjectableDeclaration; +} + +/** + * Prep interfaces for a future Ed25519 challenge/response admin auth flow. + * No crypto is implemented here - verification is delegated to an injectable + * service so the real implementation (native WebCrypto Ed25519 support, or a + * backend verification call) can be swapped in once the backend API exists, + * without touching AdminAuthService or components. + */ +interface Ed25519Challenge { + nonce: string; + timestamp: string; + /** Opaque challenge payload the client must sign with its private key. */ + payload: string; +} +interface Ed25519SignedResponse { + challenge: Ed25519Challenge; + publicKey: string; + signature: string; +} +interface Ed25519VerificationResult { + valid: boolean; + reason?: string; +} +declare abstract class Ed25519VerificationService { + abstract requestChallenge(): Observable; + abstract verify(response: Ed25519SignedResponse): Observable; +} + +/** + * Default DI binding for Ed25519VerificationService until the backend ships + * the real challenge/verify endpoints. Intentionally fails closed (throws) + * rather than pretending to verify anything, so accidental use in a login + * path is loud instead of silently accepting unsigned sessions. + */ +declare class NoopEd25519VerificationService implements Ed25519VerificationService { + requestChallenge(): Observable; + verify(_response: Ed25519SignedResponse): Observable; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; + static ɵprov: _angular_core.ɵɵInjectableDeclaration; +} + +export { AUTH_API_URL, AdminAuthService, AuthApiService, AuthFacade, AuthMarketplaceContext, AuthService$1 as AuthService, AuthService as Ed25519AuthService, Ed25519KeypairService, Ed25519VerificationService, HttpMarketplacesAuthGateway, JwtService, MARKETPLACES_AUTH_CONFIG, MARKETPLACES_AUTH_GATEWAY, MARKETPLACE_DOMAIN_HEADER, MarketplacesAuthComponent, NoopEd25519VerificationService, PermissionService, ROLE_PERMISSIONS, SessionService, TELEGRAM_BOT_USERNAME, TelegramSessionApiService, adminAuthGuard, adminAuthHeadersInterceptor, authErrorCodeFromBackendCode, authErrorCodeFromStatus, normalizeMarketplaceDomain, provideMarketplacesAuth }; +export type { AdminAuthStatus, AdminRole, AuthChallenge, AuthError, AuthErrorCode, AuthFailure, AuthMethod, AuthMode, AuthResult, AuthSession, AuthStatus, AuthTokenPair, CredentialLogin, Ed25519Challenge, Ed25519SignedResponse, Ed25519VerificationResult, ExternalAuthStart, JwtClaims, LoginPhase, MarketplacesAuthConfig, MarketplacesAuthGateway, Permission, RefreshTokenRequest, SessionStatus, VerifySignatureRequest, WebSessionStart }; diff --git a/dist/util/guid.util.d.ts b/dist/util/guid.util.d.ts deleted file mode 100644 index 810d3ac..0000000 --- a/dist/util/guid.util.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/** RFC4122 v4-ish GUID, using crypto when available. Shared by customer and admin session creation. */ -export declare function generateGuid(): string; diff --git a/dist/util/guid.util.js b/dist/util/guid.util.js deleted file mode 100644 index 19c7d8c..0000000 --- a/dist/util/guid.util.js +++ /dev/null @@ -1,19 +0,0 @@ -/** RFC4122 v4-ish GUID, using crypto when available. Shared by customer and admin session creation. */ -export function generateGuid() { - if (globalThis.crypto?.randomUUID) { - return globalThis.crypto.randomUUID(); - } - const bytes = new Uint8Array(16); - if (globalThis.crypto?.getRandomValues) { - globalThis.crypto.getRandomValues(bytes); - } - else { - for (let index = 0; index < bytes.length; index++) { - bytes[index] = Math.floor(Math.random() * 256); - } - } - bytes[6] = (bytes[6] & 0x0f) | 0x40; - bytes[8] = (bytes[8] & 0x3f) | 0x80; - const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')); - return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`; -} diff --git a/package.json b/package.json index 3b86686..de48c21 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,27 @@ { "name": "@marketplaces/auth", "version": "0.1.0", - "description": "Shared customer + admin auth client (Telegram QR/session, Ed25519 admin verification, guards, interceptors) for marketplaces projects.", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "description": "Standalone Angular authentication UI and client for marketplaces projects.", + "module": "dist/fesm2022/marketplaces-auth.mjs", + "typings": "dist/types/marketplaces-auth.d.ts", "files": ["dist"], + "scripts": { + "build": "ng-packagr -p ng-package.json -c tsconfig.json", + "test": "node --test test/*.test.mjs" + }, + "dependencies": { + "qrcode": "^1.5.4", + "tslib": "^2.8.0" + }, "peerDependencies": { "@angular/core": ">=22.0.0", "@angular/common": ">=22.0.0", + "@angular/forms": ">=22.0.0", "@angular/router": ">=22.0.0", "rxjs": ">=7.8.0" }, + "publishConfig": { + "access": "restricted" + }, "license": "UNLICENSED" }