feat(core): Add pluggable auth providers, storage hooks, multi-upstream cache awareness, and PyPI/RubyGems protocol implementations
This commit is contained in:
@@ -1,109 +1,79 @@
|
||||
import type { IAuthConfig, IAuthToken, ICredentials, TRegistryProtocol } from './interfaces.core.js';
|
||||
import * as crypto from 'crypto';
|
||||
import type { IAuthProvider, ITokenOptions } from './interfaces.auth.js';
|
||||
import { DefaultAuthProvider } from './classes.defaultauthprovider.js';
|
||||
|
||||
/**
|
||||
* Unified authentication manager for all registry protocols
|
||||
* Handles both NPM UUID tokens and OCI JWT tokens
|
||||
* Unified authentication manager for all registry protocols.
|
||||
* Delegates to a pluggable IAuthProvider for actual auth operations.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Use default in-memory provider
|
||||
* const auth = new AuthManager(config);
|
||||
*
|
||||
* // Use custom provider (LDAP, OAuth, etc.)
|
||||
* const auth = new AuthManager(config, new LdapAuthProvider(ldapClient));
|
||||
* ```
|
||||
*/
|
||||
export class AuthManager {
|
||||
private tokenStore: Map<string, IAuthToken> = new Map();
|
||||
private userCredentials: Map<string, string> = new Map(); // username -> password hash (mock)
|
||||
private provider: IAuthProvider;
|
||||
|
||||
constructor(private config: IAuthConfig) {}
|
||||
constructor(
|
||||
private config: IAuthConfig,
|
||||
provider?: IAuthProvider
|
||||
) {
|
||||
// Use provided provider or default in-memory implementation
|
||||
this.provider = provider || new DefaultAuthProvider(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the auth manager
|
||||
*/
|
||||
public async init(): Promise<void> {
|
||||
// Initialize token store (in-memory for now)
|
||||
// In production, this could be Redis or a database
|
||||
if (this.provider.init) {
|
||||
await this.provider.init();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// UUID TOKEN CREATION (Base method for NPM, Maven, etc.)
|
||||
// UNIFIED AUTHENTICATION (Delegated to Provider)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Create a UUID-based token with custom scopes (base method)
|
||||
* @param userId - User ID
|
||||
* @param protocol - Protocol type
|
||||
* @param scopes - Permission scopes
|
||||
* @param readonly - Whether the token is readonly
|
||||
* @returns UUID token string
|
||||
* Authenticate user credentials
|
||||
* @param credentials - Username and password
|
||||
* @returns User ID or null
|
||||
*/
|
||||
private async createUuidToken(
|
||||
userId: string,
|
||||
protocol: TRegistryProtocol,
|
||||
scopes: string[],
|
||||
readonly: boolean = false
|
||||
): Promise<string> {
|
||||
const token = this.generateUuid();
|
||||
const authToken: IAuthToken = {
|
||||
type: protocol,
|
||||
userId,
|
||||
scopes,
|
||||
readonly,
|
||||
metadata: {
|
||||
created: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
this.tokenStore.set(token, authToken);
|
||||
return token;
|
||||
public async authenticate(credentials: ICredentials): Promise<string | null> {
|
||||
return this.provider.authenticate(credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic protocol token creation (internal helper)
|
||||
* @param userId - User ID
|
||||
* @param protocol - Protocol type (npm, maven, composer, etc.)
|
||||
* @param readonly - Whether the token is readonly
|
||||
* @returns UUID token string
|
||||
*/
|
||||
private async createProtocolToken(
|
||||
userId: string,
|
||||
protocol: TRegistryProtocol,
|
||||
readonly: boolean
|
||||
): Promise<string> {
|
||||
const scopes = readonly
|
||||
? [`${protocol}:*:*:read`]
|
||||
: [`${protocol}:*:*:*`];
|
||||
return this.createUuidToken(userId, protocol, scopes, readonly);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic protocol token validation (internal helper)
|
||||
* @param token - UUID token string
|
||||
* @param protocol - Expected protocol type
|
||||
* Validate any token (NPM, Maven, OCI, PyPI, RubyGems, Composer, Cargo)
|
||||
* @param tokenString - Token string (UUID or JWT)
|
||||
* @param protocol - Expected protocol type (optional, improves performance)
|
||||
* @returns Auth token object or null
|
||||
*/
|
||||
private async validateProtocolToken(
|
||||
token: string,
|
||||
protocol: TRegistryProtocol
|
||||
public async validateToken(
|
||||
tokenString: string,
|
||||
protocol?: TRegistryProtocol
|
||||
): Promise<IAuthToken | null> {
|
||||
if (!this.isValidUuid(token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const authToken = this.tokenStore.get(token);
|
||||
if (!authToken || authToken.type !== protocol) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check expiration if set
|
||||
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
|
||||
this.tokenStore.delete(token);
|
||||
return null;
|
||||
}
|
||||
|
||||
return authToken;
|
||||
return this.provider.validateToken(tokenString, protocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic protocol token revocation (internal helper)
|
||||
* @param token - UUID token string
|
||||
* Check if token has permission for an action
|
||||
* @param token - Auth token (or null for anonymous)
|
||||
* @param resource - Resource being accessed (e.g., "npm:package:foo")
|
||||
* @param action - Action being performed (read, write, push, pull, delete)
|
||||
* @returns true if authorized
|
||||
*/
|
||||
private async revokeProtocolToken(token: string): Promise<void> {
|
||||
this.tokenStore.delete(token);
|
||||
public async authorize(
|
||||
token: IAuthToken | null,
|
||||
resource: string,
|
||||
action: string
|
||||
): Promise<boolean> {
|
||||
return this.provider.authorize(token, resource, action);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -120,7 +90,7 @@ export class AuthManager {
|
||||
if (!this.config.npmTokens.enabled) {
|
||||
throw new Error('NPM tokens are not enabled');
|
||||
}
|
||||
return this.createProtocolToken(userId, 'npm', readonly);
|
||||
return this.provider.createToken(userId, 'npm', { readonly });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +99,7 @@ export class AuthManager {
|
||||
* @returns Auth token object or null
|
||||
*/
|
||||
public async validateNpmToken(token: string): Promise<IAuthToken | null> {
|
||||
return this.validateProtocolToken(token, 'npm');
|
||||
return this.provider.validateToken(token, 'npm');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,7 +107,7 @@ export class AuthManager {
|
||||
* @param token - NPM UUID token
|
||||
*/
|
||||
public async revokeNpmToken(token: string): Promise<void> {
|
||||
return this.revokeProtocolToken(token);
|
||||
return this.provider.revokeToken(token);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,20 +119,12 @@ export class AuthManager {
|
||||
key: string;
|
||||
readonly: boolean;
|
||||
created: string;
|
||||
protocol?: TRegistryProtocol;
|
||||
}>> {
|
||||
const tokens: Array<{key: string; readonly: boolean; created: string}> = [];
|
||||
|
||||
for (const [token, authToken] of this.tokenStore.entries()) {
|
||||
if (authToken.userId === userId) {
|
||||
tokens.push({
|
||||
key: this.hashToken(token),
|
||||
readonly: authToken.readonly || false,
|
||||
created: authToken.metadata?.created || 'unknown',
|
||||
});
|
||||
}
|
||||
if (this.provider.listUserTokens) {
|
||||
return this.provider.listUserTokens(userId);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
return [];
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -174,39 +136,17 @@ export class AuthManager {
|
||||
* @param userId - User ID
|
||||
* @param scopes - Permission scopes
|
||||
* @param expiresIn - Expiration time in seconds
|
||||
* @returns JWT token string (HMAC-SHA256 signed)
|
||||
* @returns JWT token string
|
||||
*/
|
||||
public async createOciToken(
|
||||
userId: string,
|
||||
scopes: string[],
|
||||
expiresIn: number = 3600
|
||||
): Promise<string> {
|
||||
if (!this.config.ociTokens.enabled) {
|
||||
if (!this.config.ociTokens?.enabled) {
|
||||
throw new Error('OCI tokens are not enabled');
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const payload = {
|
||||
iss: this.config.ociTokens.realm,
|
||||
sub: userId,
|
||||
aud: this.config.ociTokens.service,
|
||||
exp: now + expiresIn,
|
||||
nbf: now,
|
||||
iat: now,
|
||||
access: this.scopesToOciAccess(scopes),
|
||||
};
|
||||
|
||||
// Create JWT with HMAC-SHA256 signature
|
||||
const header = { alg: 'HS256', typ: 'JWT' };
|
||||
const headerB64 = Buffer.from(JSON.stringify(header)).toString('base64url');
|
||||
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
||||
|
||||
const signature = crypto
|
||||
.createHmac('sha256', this.config.jwtSecret)
|
||||
.update(`${headerB64}.${payloadB64}`)
|
||||
.digest('base64url');
|
||||
|
||||
return `${headerB64}.${payloadB64}.${signature}`;
|
||||
return this.provider.createToken(userId, 'oci', { scopes, expiresIn });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,80 +155,7 @@ export class AuthManager {
|
||||
* @returns Auth token object or null
|
||||
*/
|
||||
public async validateOciToken(jwt: string): Promise<IAuthToken | null> {
|
||||
try {
|
||||
const parts = jwt.split('.');
|
||||
if (parts.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [headerB64, payloadB64, signatureB64] = parts;
|
||||
|
||||
// Verify signature
|
||||
const expectedSignature = crypto
|
||||
.createHmac('sha256', this.config.jwtSecret)
|
||||
.update(`${headerB64}.${payloadB64}`)
|
||||
.digest('base64url');
|
||||
|
||||
if (signatureB64 !== expectedSignature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Decode and parse payload
|
||||
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf-8'));
|
||||
|
||||
// Check expiration
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (payload.exp && payload.exp < now) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check not-before time
|
||||
if (payload.nbf && payload.nbf > now) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert to unified token format
|
||||
const scopes = this.ociAccessToScopes(payload.access || []);
|
||||
|
||||
return {
|
||||
type: 'oci',
|
||||
userId: payload.sub,
|
||||
scopes,
|
||||
expiresAt: payload.exp ? new Date(payload.exp * 1000) : undefined,
|
||||
metadata: {
|
||||
iss: payload.iss,
|
||||
aud: payload.aud,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// UNIFIED AUTHENTICATION
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Authenticate user credentials
|
||||
* @param credentials - Username and password
|
||||
* @returns User ID or null
|
||||
*/
|
||||
public async authenticate(credentials: ICredentials): Promise<string | null> {
|
||||
// Mock authentication - in production, verify against database
|
||||
const storedPassword = this.userCredentials.get(credentials.username);
|
||||
|
||||
if (!storedPassword) {
|
||||
// Auto-register for testing (remove in production)
|
||||
this.userCredentials.set(credentials.username, credentials.password);
|
||||
return credentials.username;
|
||||
}
|
||||
|
||||
if (storedPassword === credentials.password) {
|
||||
return credentials.username;
|
||||
}
|
||||
|
||||
return null;
|
||||
return this.provider.validateToken(jwt, 'oci');
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -302,7 +169,7 @@ export class AuthManager {
|
||||
* @returns Maven UUID token
|
||||
*/
|
||||
public async createMavenToken(userId: string, readonly: boolean = false): Promise<string> {
|
||||
return this.createProtocolToken(userId, 'maven', readonly);
|
||||
return this.provider.createToken(userId, 'maven', { readonly });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,7 +178,7 @@ export class AuthManager {
|
||||
* @returns Auth token object or null
|
||||
*/
|
||||
public async validateMavenToken(token: string): Promise<IAuthToken | null> {
|
||||
return this.validateProtocolToken(token, 'maven');
|
||||
return this.provider.validateToken(token, 'maven');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,7 +186,7 @@ export class AuthManager {
|
||||
* @param token - Maven UUID token
|
||||
*/
|
||||
public async revokeMavenToken(token: string): Promise<void> {
|
||||
return this.revokeProtocolToken(token);
|
||||
return this.provider.revokeToken(token);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -333,7 +200,7 @@ export class AuthManager {
|
||||
* @returns Composer UUID token
|
||||
*/
|
||||
public async createComposerToken(userId: string, readonly: boolean = false): Promise<string> {
|
||||
return this.createProtocolToken(userId, 'composer', readonly);
|
||||
return this.provider.createToken(userId, 'composer', { readonly });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -342,7 +209,7 @@ export class AuthManager {
|
||||
* @returns Auth token object or null
|
||||
*/
|
||||
public async validateComposerToken(token: string): Promise<IAuthToken | null> {
|
||||
return this.validateProtocolToken(token, 'composer');
|
||||
return this.provider.validateToken(token, 'composer');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,7 +217,7 @@ export class AuthManager {
|
||||
* @param token - Composer UUID token
|
||||
*/
|
||||
public async revokeComposerToken(token: string): Promise<void> {
|
||||
return this.revokeProtocolToken(token);
|
||||
return this.provider.revokeToken(token);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -364,7 +231,7 @@ export class AuthManager {
|
||||
* @returns Cargo UUID token
|
||||
*/
|
||||
public async createCargoToken(userId: string, readonly: boolean = false): Promise<string> {
|
||||
return this.createProtocolToken(userId, 'cargo', readonly);
|
||||
return this.provider.createToken(userId, 'cargo', { readonly });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -373,7 +240,7 @@ export class AuthManager {
|
||||
* @returns Auth token object or null
|
||||
*/
|
||||
public async validateCargoToken(token: string): Promise<IAuthToken | null> {
|
||||
return this.validateProtocolToken(token, 'cargo');
|
||||
return this.provider.validateToken(token, 'cargo');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -381,7 +248,7 @@ export class AuthManager {
|
||||
* @param token - Cargo UUID token
|
||||
*/
|
||||
public async revokeCargoToken(token: string): Promise<void> {
|
||||
return this.revokeProtocolToken(token);
|
||||
return this.provider.revokeToken(token);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -395,7 +262,7 @@ export class AuthManager {
|
||||
* @returns PyPI UUID token
|
||||
*/
|
||||
public async createPypiToken(userId: string, readonly: boolean = false): Promise<string> {
|
||||
return this.createProtocolToken(userId, 'pypi', readonly);
|
||||
return this.provider.createToken(userId, 'pypi', { readonly });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -404,7 +271,7 @@ export class AuthManager {
|
||||
* @returns Auth token object or null
|
||||
*/
|
||||
public async validatePypiToken(token: string): Promise<IAuthToken | null> {
|
||||
return this.validateProtocolToken(token, 'pypi');
|
||||
return this.provider.validateToken(token, 'pypi');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -412,7 +279,7 @@ export class AuthManager {
|
||||
* @param token - PyPI UUID token
|
||||
*/
|
||||
public async revokePypiToken(token: string): Promise<void> {
|
||||
return this.revokeProtocolToken(token);
|
||||
return this.provider.revokeToken(token);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -426,7 +293,7 @@ export class AuthManager {
|
||||
* @returns RubyGems UUID token
|
||||
*/
|
||||
public async createRubyGemsToken(userId: string, readonly: boolean = false): Promise<string> {
|
||||
return this.createProtocolToken(userId, 'rubygems', readonly);
|
||||
return this.provider.createToken(userId, 'rubygems', { readonly });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -435,7 +302,7 @@ export class AuthManager {
|
||||
* @returns Auth token object or null
|
||||
*/
|
||||
public async validateRubyGemsToken(token: string): Promise<IAuthToken | null> {
|
||||
return this.validateProtocolToken(token, 'rubygems');
|
||||
return this.provider.validateToken(token, 'rubygems');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -443,211 +310,6 @@ export class AuthManager {
|
||||
* @param token - RubyGems UUID token
|
||||
*/
|
||||
public async revokeRubyGemsToken(token: string): Promise<void> {
|
||||
return this.revokeProtocolToken(token);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// UNIFIED AUTHENTICATION
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Validate any token (NPM, Maven, OCI, PyPI, RubyGems, Composer, Cargo)
|
||||
* Optimized: O(1) lookup when protocol hint provided
|
||||
* @param tokenString - Token string (UUID or JWT)
|
||||
* @param protocol - Expected protocol type (optional, improves performance)
|
||||
* @returns Auth token object or null
|
||||
*/
|
||||
public async validateToken(
|
||||
tokenString: string,
|
||||
protocol?: TRegistryProtocol
|
||||
): Promise<IAuthToken | null> {
|
||||
// OCI uses JWT (contains dots), not UUID - check first if OCI is expected
|
||||
if (protocol === 'oci' || tokenString.includes('.')) {
|
||||
const ociToken = await this.validateOciToken(tokenString);
|
||||
if (ociToken && (!protocol || protocol === 'oci')) {
|
||||
return ociToken;
|
||||
}
|
||||
// If protocol was explicitly OCI but validation failed, return null
|
||||
if (protocol === 'oci') {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// UUID-based tokens: single O(1) Map lookup
|
||||
if (this.isValidUuid(tokenString)) {
|
||||
const authToken = this.tokenStore.get(tokenString);
|
||||
if (authToken) {
|
||||
// If protocol specified, verify it matches
|
||||
if (protocol && authToken.type !== protocol) {
|
||||
return null;
|
||||
}
|
||||
// Check expiration
|
||||
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
|
||||
this.tokenStore.delete(tokenString);
|
||||
return null;
|
||||
}
|
||||
return authToken;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token has permission for an action
|
||||
* @param token - Auth token
|
||||
* @param resource - Resource being accessed (e.g., "package:foo" or "repository:bar")
|
||||
* @param action - Action being performed (read, write, push, pull, delete)
|
||||
* @returns true if authorized
|
||||
*/
|
||||
public async authorize(
|
||||
token: IAuthToken | null,
|
||||
resource: string,
|
||||
action: string
|
||||
): Promise<boolean> {
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check readonly flag
|
||||
if (token.readonly && ['write', 'push', 'delete'].includes(action)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check scopes
|
||||
for (const scope of token.scopes) {
|
||||
if (this.matchesScope(scope, resource, action)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// HELPER METHODS
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Check if a scope matches a resource and action
|
||||
* Scope format: "{protocol}:{type}:{name}:{action}"
|
||||
* Examples:
|
||||
* - "npm:*:*" - All NPM access
|
||||
* - "npm:package:foo:*" - All actions on package foo
|
||||
* - "npm:package:foo:read" - Read-only on package foo
|
||||
* - "oci:repository:*:pull" - Pull from any OCI repo
|
||||
*/
|
||||
private matchesScope(scope: string, resource: string, action: string): boolean {
|
||||
const scopeParts = scope.split(':');
|
||||
const resourceParts = resource.split(':');
|
||||
|
||||
// Scope must have at least protocol:type:name:action
|
||||
if (scopeParts.length < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [scopeProtocol, scopeType, scopeName, scopeAction] = scopeParts;
|
||||
const [resourceProtocol, resourceType, resourceName] = resourceParts;
|
||||
|
||||
// Check protocol
|
||||
if (scopeProtocol !== '*' && scopeProtocol !== resourceProtocol) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check type
|
||||
if (scopeType !== '*' && scopeType !== resourceType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check name
|
||||
if (scopeName !== '*' && scopeName !== resourceName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check action
|
||||
if (scopeAction !== '*' && scopeAction !== action) {
|
||||
// Map action aliases
|
||||
const actionAliases: Record<string, string[]> = {
|
||||
read: ['pull', 'get'],
|
||||
write: ['push', 'put', 'post'],
|
||||
};
|
||||
|
||||
const aliases = actionAliases[scopeAction] || [];
|
||||
if (!aliases.includes(action)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert unified scopes to OCI access array
|
||||
*/
|
||||
private scopesToOciAccess(scopes: string[]): Array<{
|
||||
type: string;
|
||||
name: string;
|
||||
actions: string[];
|
||||
}> {
|
||||
const access: Array<{type: string; name: string; actions: string[]}> = [];
|
||||
|
||||
for (const scope of scopes) {
|
||||
const parts = scope.split(':');
|
||||
if (parts.length >= 4 && parts[0] === 'oci') {
|
||||
access.push({
|
||||
type: parts[1],
|
||||
name: parts[2],
|
||||
actions: [parts[3]],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return access;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert OCI access array to unified scopes
|
||||
*/
|
||||
private ociAccessToScopes(access: Array<{
|
||||
type: string;
|
||||
name: string;
|
||||
actions: string[];
|
||||
}>): string[] {
|
||||
const scopes: string[] = [];
|
||||
|
||||
for (const item of access) {
|
||||
for (const action of item.actions) {
|
||||
scopes.push(`oci:${item.type}:${item.name}:${action}`);
|
||||
}
|
||||
}
|
||||
|
||||
return scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate UUID for NPM tokens
|
||||
*/
|
||||
private generateUuid(): string {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if string is a valid UUID
|
||||
*/
|
||||
private isValidUuid(str: string): boolean {
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash a token for identification (SHA-512 mock)
|
||||
*/
|
||||
private hashToken(token: string): string {
|
||||
// In production, use actual SHA-512
|
||||
return `sha512-${token.substring(0, 16)}...`;
|
||||
return this.provider.revokeToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user