diff --git a/changelog.md b/changelog.md index 765f7cb..4082dd9 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2026-03-20 - 1.4.2 - fix(registry) +align registry integrations with updated auth, storage, repository, and audit models + +- update smartregistry auth and storage provider implementations to match the current request, token, and storage hook APIs +- fix audit events for auth provider, platform settings, and external authentication flows to use dedicated event types +- adapt repository, organization, user, and package handlers to renamed model fields and revised repository visibility/protocol data +- add missing repository and team model fields plus helper methods needed by the updated API and permission flows +- correct AES-GCM crypto buffer handling and package version checksum mapping + ## 2026-03-20 - 1.4.1 - fix(repo) no changes to commit diff --git a/deno.json b/deno.json index 104de34..e103eb8 100644 --- a/deno.json +++ b/deno.json @@ -35,7 +35,6 @@ "@push.rocks/smartdelay": "npm:@push.rocks/smartdelay@^3.0.5", "@push.rocks/smartrx": "npm:@push.rocks/smartrx@^3.0.10", "@push.rocks/smartcli": "npm:@push.rocks/smartcli@^4.0.20", - "@push.rocks/smartarchive": "npm:@push.rocks/smartarchive@^5.2.1", "@push.rocks/qenv": "npm:@push.rocks/qenv@^6.1.3", "@tsclass/tsclass": "npm:@tsclass/tsclass@^9.5.0", "@std/path": "jsr:@std/path@^1.0.0", diff --git a/deno.lock b/deno.lock index 4b40e6f..bb877be 100644 --- a/deno.lock +++ b/deno.lock @@ -20,7 +20,6 @@ "jsr:@std/testing@*": "1.0.17", "npm:@git.zone/tsdeno@^1.2.0": "1.2.0", "npm:@push.rocks/qenv@^6.1.3": "6.1.3", - "npm:@push.rocks/smartarchive@^5.2.1": "5.2.1", "npm:@push.rocks/smartbucket@^4.5.1": "4.5.1", "npm:@push.rocks/smartcli@^4.0.20": "4.0.20", "npm:@push.rocks/smartcrypto@^2.0.4": "2.0.4", @@ -805,7 +804,7 @@ "integrity": "sha512-kW0ZUGyf1e4nwloVwBQjNId+MzgTcNS834C+RxH21i1NqyOubbpWZtJtPP+K+s35nSJRyCTy3ICfBMdDBTAm2w==", "dependencies": [ "@push.rocks/lik", - "@push.rocks/smartfile@11.2.7", + "@push.rocks/smartfile", "@push.rocks/smartjson@5.2.0", "@push.rocks/smartpath@6.0.0", "@push.rocks/smartpromise", @@ -818,7 +817,7 @@ "integrity": "sha512-snLpSHwaQ5OXlZzF1KX/FY71W5LwajjBzor82Vue0smjEPnSeUPY5/JcVdMwtdprdJe13pc/EQQuIiL/zw4/yg==", "dependencies": [ "@push.rocks/qenv", - "@push.rocks/smartfile@11.2.7", + "@push.rocks/smartfile", "@push.rocks/smartjson@5.2.0", "@push.rocks/smartlog", "@push.rocks/smartpath@6.0.0", @@ -834,31 +833,12 @@ "dependencies": [ "@api.global/typedrequest", "@configvault.io/interfaces", - "@push.rocks/smartfile@11.2.7", + "@push.rocks/smartfile", "@push.rocks/smartlog", "@push.rocks/smartpath@6.0.0" ], "tarball": "https://verdaccio.lossless.digital/@push.rocks/qenv/-/qenv-6.1.3.tgz" }, - "@push.rocks/smartarchive@5.2.1": { - "integrity": "sha512-TNv5q6QuBRX7jrzffiyb6A8AALNAr0kyAcJswa0l3ahBP1Q6zszNo9xOVXmW2gKX2KShtO/Y+Cn0i46n8lbnaQ==", - "dependencies": [ - "@push.rocks/smartdelay", - "@push.rocks/smartfile@13.1.2", - "@push.rocks/smartpath@6.0.0", - "@push.rocks/smartpromise", - "@push.rocks/smartrequest@5.0.1", - "@push.rocks/smartrx", - "@push.rocks/smartstream", - "@push.rocks/smartunique", - "@push.rocks/smarturl", - "fflate", - "file-type@21.3.3", - "modern-tar", - "tar-stream" - ], - "tarball": "https://verdaccio.lossless.digital/@push.rocks/smartarchive/-/smartarchive-5.2.1.tgz" - }, "@push.rocks/smartbucket@4.5.1": { "integrity": "sha512-mce9x7YH68ZgNLJU0ZWflt03AlS+jMe9BNZNhwM0N5T87q1uhNFvjFzkvyhBj8XO6g4CTQvQGxPuJXZqD5aUsg==", "dependencies": [ @@ -1005,26 +985,6 @@ ], "tarball": "https://verdaccio.lossless.digital/@push.rocks/smartfile/-/smartfile-11.2.7.tgz" }, - "@push.rocks/smartfile@13.1.2": { - "integrity": "sha512-DaEhwmnGEpX4coeeToaw4cZe3pNBhH7CY1iGr+d3pIXihozREvzzAR9/0i2r7bUXXL5+Lgy8YYIk5ZS+fwxMKA==", - "dependencies": [ - "@push.rocks/lik", - "@push.rocks/smartdelay", - "@push.rocks/smartfile-interfaces", - "@push.rocks/smartfs", - "@push.rocks/smarthash", - "@push.rocks/smartjson@5.2.0", - "@push.rocks/smartmime", - "@push.rocks/smartpath@6.0.0", - "@push.rocks/smartpromise", - "@push.rocks/smartrequest@4.4.2", - "@push.rocks/smartstream", - "@types/js-yaml@4.0.9", - "glob", - "js-yaml@4.1.1" - ], - "tarball": "https://verdaccio.lossless.digital/@push.rocks/smartfile/-/smartfile-13.1.2.tgz" - }, "@push.rocks/smartfs@1.5.0": { "integrity": "sha512-QwMD44HgX3d9PPxUwR0uS+0PEMtesKvKbZR+s4pezL2er6oPneKJMLkO6TJPvJ38nug6Lmlk9Bu7UrwR2kS3Vw==", "dependencies": [ @@ -1091,7 +1051,7 @@ "@push.rocks/consolecolor", "@push.rocks/isounique", "@push.rocks/smartclickhouse", - "@push.rocks/smartfile@11.2.7", + "@push.rocks/smartfile", "@push.rocks/smarthash", "@push.rocks/smartpromise", "@push.rocks/smarttime", @@ -1127,7 +1087,7 @@ "integrity": "sha512-mG6lRBLr5nF+GLZmgCcdjhdDsmTtJWBFZDCa1eJ8Au9TvUzbPW0fY5aqJBb3UwfyZzH6St8Th9cJSXjagOQkYA==", "dependencies": [ "@types/mime-types", - "file-type@19.6.0", + "file-type", "mime" ], "tarball": "https://verdaccio.lossless.digital/@push.rocks/smartmime/-/smartmime-2.0.4.tgz" @@ -1894,14 +1854,6 @@ "integrity": "sha512-ypeB0FuHLHOCQXW4d0RQ69txPJJH+1CHcpsZIUdcv2t1vR0IVyQr2vHihtde9UOXhjzqEnUphWon/UcJNsa0YA==", "tarball": "https://verdaccio.lossless.digital/@tempfix/lenis/-/lenis-1.3.20.tgz" }, - "@tokenizer/inflate@0.4.1": { - "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", - "dependencies": [ - "debug", - "token-types" - ], - "tarball": "https://verdaccio.lossless.digital/@tokenizer/inflate/-/inflate-0.4.1.tgz" - }, "@tokenizer/token@0.3.0": { "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "tarball": "https://verdaccio.lossless.digital/@tokenizer/token/-/token-0.3.0.tgz" @@ -2483,30 +2435,16 @@ ], "tarball": "https://verdaccio.lossless.digital/fault/-/fault-2.0.1.tgz" }, - "fflate@0.8.2": { - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "tarball": "https://verdaccio.lossless.digital/fflate/-/fflate-0.8.2.tgz" - }, "file-type@19.6.0": { "integrity": "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==", "dependencies": [ "get-stream", - "strtok3@9.1.1", + "strtok3", "token-types", "uint8array-extras" ], "tarball": "https://verdaccio.lossless.digital/file-type/-/file-type-19.6.0.tgz" }, - "file-type@21.3.3": { - "integrity": "sha512-pNwbwz8c3aZ+GvbJnIsCnDjKvgCZLHxkFWLEFxU3RMa+Ey++ZSEfisvsWQMcdys6PpxQjWUOIDi1fifXsW3YRg==", - "dependencies": [ - "@tokenizer/inflate", - "strtok3@10.3.4", - "token-types", - "uint8array-extras" - ], - "tarball": "https://verdaccio.lossless.digital/file-type/-/file-type-21.3.3.tgz" - }, "find-cache-dir@3.3.2": { "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "dependencies": [ @@ -3370,10 +3308,6 @@ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "tarball": "https://verdaccio.lossless.digital/minipass/-/minipass-7.1.3.tgz" }, - "modern-tar@0.7.5": { - "integrity": "sha512-YTefgdpKKFgoTDbEUqXqgUJct2OG6/4hs4XWLsxcHkDLj/x/V8WmKIRppPnXP5feQ7d1vuYWSp3qKkxfwaFaxA==", - "tarball": "https://verdaccio.lossless.digital/modern-tar/-/modern-tar-0.7.5.tgz" - }, "mongodb-connection-string-url@3.0.2": { "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", "dependencies": [ @@ -3738,13 +3672,6 @@ "integrity": "sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==", "tarball": "https://verdaccio.lossless.digital/strnum/-/strnum-2.2.1.tgz" }, - "strtok3@10.3.4": { - "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", - "dependencies": [ - "@tokenizer/token" - ], - "tarball": "https://verdaccio.lossless.digital/strtok3/-/strtok3-10.3.4.tgz" - }, "strtok3@9.1.1": { "integrity": "sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==", "dependencies": [ @@ -4038,7 +3965,6 @@ "jsr:@std/http@1", "jsr:@std/path@1", "npm:@push.rocks/qenv@^6.1.3", - "npm:@push.rocks/smartarchive@^5.2.1", "npm:@push.rocks/smartbucket@^4.5.1", "npm:@push.rocks/smartcli@^4.0.20", "npm:@push.rocks/smartcrypto@^2.0.4", diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index abc04c6..359fdf7 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@stack.gallery/registry', - version: '1.4.1', + version: '1.4.2', description: 'Enterprise-grade multi-protocol package registry' } diff --git a/ts/api/handlers/admin.auth.api.ts b/ts/api/handlers/admin.auth.api.ts index c4b1750..f02ff15 100644 --- a/ts/api/handlers/admin.auth.api.ts +++ b/ts/api/handlers/admin.auth.api.ts @@ -109,7 +109,7 @@ export class AdminAuthApi { }, attributeMapping: body.attributeMapping, provisioning: body.provisioning, - createdById: ctx.actor!.userId, + createdById: ctx.actor!.userId!, }); } else if (body.type === 'ldap' && body.ldapConfig) { // Encrypt bind password @@ -124,7 +124,7 @@ export class AdminAuthApi { }, attributeMapping: body.attributeMapping, provisioning: body.provisioning, - createdById: ctx.actor!.userId, + createdById: ctx.actor!.userId!, }); } else { return { @@ -138,11 +138,10 @@ export class AdminAuthApi { actorId: ctx.actor!.userId, actorType: 'user', actorIp: ctx.ip, - }).log('ORGANIZATION_CREATED', 'system', { + }).log('AUTH_PROVIDER_CREATED', 'auth_provider', { resourceId: provider.id, success: true, metadata: { - action: 'auth_provider_created', providerName: provider.name, providerType: provider.type, }, @@ -270,11 +269,10 @@ export class AdminAuthApi { actorId: ctx.actor!.userId, actorType: 'user', actorIp: ctx.ip, - }).log('ORGANIZATION_UPDATED', 'system', { + }).log('AUTH_PROVIDER_UPDATED', 'auth_provider', { resourceId: provider.id, success: true, metadata: { - action: 'auth_provider_updated', providerName: provider.name, }, }); @@ -321,11 +319,10 @@ export class AdminAuthApi { actorId: ctx.actor!.userId, actorType: 'user', actorIp: ctx.ip, - }).log('ORGANIZATION_DELETED', 'system', { + }).log('AUTH_PROVIDER_DELETED', 'auth_provider', { resourceId: provider.id, success: true, metadata: { - action: 'auth_provider_disabled', providerName: provider.name, }, }); @@ -360,11 +357,10 @@ export class AdminAuthApi { actorId: ctx.actor!.userId, actorType: 'user', actorIp: ctx.ip, - }).log('ORGANIZATION_UPDATED', 'system', { + }).log('AUTH_PROVIDER_TESTED', 'auth_provider', { resourceId: id, success: result.success, metadata: { - action: 'auth_provider_tested', result: result.success ? 'success' : 'failure', latencyMs: result.latencyMs, error: result.error, @@ -433,12 +429,9 @@ export class AdminAuthApi { actorId: ctx.actor!.userId, actorType: 'user', actorIp: ctx.ip, - }).log('ORGANIZATION_UPDATED', 'system', { + }).log('PLATFORM_SETTINGS_UPDATED', 'platform_settings', { resourceId: 'platform-settings', success: true, - metadata: { - action: 'platform_settings_updated', - }, }); return { diff --git a/ts/api/handlers/organization.api.ts b/ts/api/handlers/organization.api.ts index 50f7b3e..22e7265 100644 --- a/ts/api/handlers/organization.api.ts +++ b/ts/api/handlers/organization.api.ts @@ -39,7 +39,13 @@ export class OrganizationApi { if (ctx.actor.user?.isSystemAdmin) { organizations = await Organization.getInstances({}); } else { - organizations = await OrganizationMember.getUserOrganizations(ctx.actor.userId); + const memberships = await OrganizationMember.getUserOrganizations(ctx.actor.userId); + const orgs: Organization[] = []; + for (const m of memberships) { + const org = await Organization.findById(m.organizationId); + if (org) orgs.push(org); + } + organizations = orgs; } return { @@ -155,8 +161,8 @@ export class OrganizationApi { membership.organizationId = org.id; membership.userId = ctx.actor.userId; membership.role = 'owner'; - membership.addedById = ctx.actor.userId; - membership.addedAt = new Date(); + membership.invitedBy = ctx.actor.userId; + membership.joinedAt = new Date(); await membership.save(); @@ -310,7 +316,7 @@ export class OrganizationApi { return { userId: m.userId, role: m.role, - addedAt: m.addedAt, + addedAt: m.joinedAt, user: user ? { username: user.username, @@ -384,8 +390,8 @@ export class OrganizationApi { membership.organizationId = org.id; membership.userId = userId; membership.role = role; - membership.addedById = ctx.actor.userId; - membership.addedAt = new Date(); + membership.invitedBy = ctx.actor.userId; + membership.joinedAt = new Date(); await membership.save(); @@ -398,7 +404,7 @@ export class OrganizationApi { body: { userId: membership.userId, role: membership.role, - addedAt: membership.addedAt, + addedAt: membership.joinedAt, }, }; } catch (error) { diff --git a/ts/api/handlers/package.api.ts b/ts/api/handlers/package.api.ts index c9e1f26..99ce84f 100644 --- a/ts/api/handlers/package.api.ts +++ b/ts/api/handlers/package.api.ts @@ -174,7 +174,7 @@ export class PackageApi { publishedAt: data.publishedAt, size: data.size, downloads: data.downloads, - checksum: data.checksum, + checksum: data.metadata?.checksum, })); return { diff --git a/ts/api/handlers/repository.api.ts b/ts/api/handlers/repository.api.ts index 39f70f2..20b3b0a 100644 --- a/ts/api/handlers/repository.api.ts +++ b/ts/api/handlers/repository.api.ts @@ -6,7 +6,7 @@ import type { IApiContext, IApiResponse } from '../router.ts'; import { PermissionService } from '../../services/permission.service.ts'; import { AuditService } from '../../services/audit.service.ts'; import { Repository, Organization } from '../../models/index.ts'; -import type { TRegistryProtocol } from '../../interfaces/auth.interfaces.ts'; +import type { TRegistryProtocol, TRepositoryVisibility } from '../../interfaces/auth.interfaces.ts'; export class RepositoryApi { private permissionService: PermissionService; @@ -26,7 +26,6 @@ export class RepositoryApi { const { orgId } = ctx.params; try { - // Get accessible repositories const repositories = await this.permissionService.getAccessibleRepositories( ctx.actor.userId, orgId @@ -38,9 +37,9 @@ export class RepositoryApi { repositories: repositories.map((repo) => ({ id: repo.id, name: repo.name, - displayName: repo.displayName, description: repo.description, - protocols: repo.protocols, + protocol: repo.protocol, + visibility: repo.visibility, isPublic: repo.isPublic, packageCount: repo.packageCount, createdAt: repo.createdAt, @@ -84,11 +83,10 @@ export class RepositoryApi { id: repo.id, organizationId: repo.organizationId, name: repo.name, - displayName: repo.displayName, description: repo.description, - protocols: repo.protocols, + protocol: repo.protocol, + visibility: repo.visibility, isPublic: repo.isPublic, - settings: repo.settings, packageCount: repo.packageCount, storageBytes: repo.storageBytes, createdAt: repo.createdAt, @@ -118,17 +116,22 @@ export class RepositoryApi { try { const body = await ctx.request.json(); - const { name, displayName, description, protocols, isPublic, settings } = body; + const { name, description, protocol, visibility } = body as { + name: string; + description?: string; + protocol?: TRegistryProtocol; + visibility?: TRepositoryVisibility; + }; if (!name) { return { status: 400, body: { error: 'Repository name is required' } }; } // Validate name format - if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(name)) { + if (!/^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/.test(name)) { return { status: 400, - body: { error: 'Name must be lowercase alphanumeric with optional hyphens' }, + body: { error: 'Name must be lowercase alphanumeric with optional dots, hyphens, or underscores' }, }; } @@ -138,30 +141,15 @@ export class RepositoryApi { return { status: 404, body: { error: 'Organization not found' } }; } - // Check if name is taken in this org - const existing = await Repository.findByName(orgId, name); - if (existing) { - return { status: 409, body: { error: 'Repository name already taken in this organization' } }; - } - - // Create repository - const repo = new Repository(); - repo.id = await Repository.getNewId(); - repo.organizationId = orgId; - repo.name = name; - repo.displayName = displayName || name; - repo.description = description; - repo.protocols = protocols || ['npm']; - repo.isPublic = isPublic ?? false; - repo.settings = settings || { - allowOverwrite: false, - immutableTags: false, - retentionDays: 0, - }; - repo.createdAt = new Date(); - repo.createdById = ctx.actor.userId; - - await repo.save(); + // Create repository using the model's factory method + const repo = await Repository.createRepository({ + organizationId: orgId, + name, + description, + protocol: protocol || 'npm', + visibility: visibility || 'private', + createdById: ctx.actor.userId, + }); // Audit log await AuditService.withContext({ @@ -177,9 +165,9 @@ export class RepositoryApi { id: repo.id, organizationId: repo.organizationId, name: repo.name, - displayName: repo.displayName, description: repo.description, - protocols: repo.protocols, + protocol: repo.protocol, + visibility: repo.visibility, isPublic: repo.isPublic, createdAt: repo.createdAt, }, @@ -217,13 +205,13 @@ export class RepositoryApi { } const body = await ctx.request.json(); - const { displayName, description, protocols, isPublic, settings } = body; + const { description, visibility } = body as { + description?: string; + visibility?: TRepositoryVisibility; + }; - if (displayName !== undefined) repo.displayName = displayName; if (description !== undefined) repo.description = description; - if (protocols !== undefined) repo.protocols = protocols; - if (isPublic !== undefined) repo.isPublic = isPublic; - if (settings !== undefined) repo.settings = { ...repo.settings, ...settings }; + if (visibility !== undefined) repo.visibility = visibility; await repo.save(); @@ -232,11 +220,10 @@ export class RepositoryApi { body: { id: repo.id, name: repo.name, - displayName: repo.displayName, description: repo.description, - protocols: repo.protocols, + protocol: repo.protocol, + visibility: repo.visibility, isPublic: repo.isPublic, - settings: repo.settings, }, }; } catch (error) { diff --git a/ts/api/handlers/user.api.ts b/ts/api/handlers/user.api.ts index 43a2a78..4d27ecf 100644 --- a/ts/api/handlers/user.api.ts +++ b/ts/api/handlers/user.api.ts @@ -137,8 +137,8 @@ export class UserApi { user.username = username; user.passwordHash = passwordHash; user.displayName = displayName || username; - user.isSystemAdmin = isSystemAdmin || false; - user.isActive = true; + user.isPlatformAdmin = isSystemAdmin || false; + user.status = 'active'; user.createdAt = new Date(); await user.save(); @@ -189,8 +189,8 @@ export class UserApi { // Only admins can change these if (ctx.actor.user?.isSystemAdmin) { - if (isActive !== undefined) user.isActive = isActive; - if (isSystemAdmin !== undefined) user.isSystemAdmin = isSystemAdmin; + if (isActive !== undefined) user.status = isActive ? 'active' : 'suspended'; + if (isSystemAdmin !== undefined) user.isPlatformAdmin = isSystemAdmin; } // Password change @@ -245,7 +245,7 @@ export class UserApi { } // Soft delete - deactivate instead of removing - user.isActive = false; + user.status = 'suspended'; await user.save(); return { diff --git a/ts/interfaces/audit.interfaces.ts b/ts/interfaces/audit.interfaces.ts index 9f4cc87..c0ea839 100644 --- a/ts/interfaces/audit.interfaces.ts +++ b/ts/interfaces/audit.interfaces.ts @@ -51,6 +51,13 @@ export type TAuditAction = | 'PACKAGE_PULLED' | 'PACKAGE_DELETED' | 'PACKAGE_DEPRECATED' + // Auth Provider Management + | 'AUTH_PROVIDER_CREATED' + | 'AUTH_PROVIDER_UPDATED' + | 'AUTH_PROVIDER_DELETED' + | 'AUTH_PROVIDER_TESTED' + // Platform Settings + | 'PLATFORM_SETTINGS_UPDATED' // Security Events | 'SECURITY_SCAN_COMPLETED' | 'SECURITY_VULNERABILITY_FOUND' @@ -65,6 +72,8 @@ export type TAuditResourceType = | 'package' | 'api_token' | 'session' + | 'auth_provider' + | 'platform_settings' | 'system'; // ============================================================================= diff --git a/ts/models/repository.permission.ts b/ts/models/repository.permission.ts index 71a2cd6..b19b7c2 100644 --- a/ts/models/repository.permission.ts +++ b/ts/models/repository.permission.ts @@ -99,6 +99,16 @@ export class RepositoryPermission extends plugins.smartdata.SmartDataDbDoc { + return await RepositoryPermission.getUserPermission(repositoryId, userId); + } + /** * Get user's direct permission on repository */ diff --git a/ts/models/repository.ts b/ts/models/repository.ts index 84c580b..2ce03c3 100644 --- a/ts/models/repository.ts +++ b/ts/models/repository.ts @@ -39,6 +39,12 @@ export class Repository extends plugins.smartdata.SmartDataDbDoc { + return await Repository.getInstance({ id }); + } + /** * Increment download count */ diff --git a/ts/models/team.ts b/ts/models/team.ts index 2d1059f..36865b3 100644 --- a/ts/models/team.ts +++ b/ts/models/team.ts @@ -25,6 +25,9 @@ export class Team extends plugins.smartdata.SmartDataDbDoc implement @plugins.smartdata.svDb() public isDefaultTeam: boolean = false; + @plugins.smartdata.svDb() + public repositoryIds: string[] = []; + @plugins.smartdata.svDb() @plugins.smartdata.index() public createdAt: Date = new Date(); diff --git a/ts/providers/auth.provider.ts b/ts/providers/auth.provider.ts index 0203fde..e5c6c42 100644 --- a/ts/providers/auth.provider.ts +++ b/ts/providers/auth.provider.ts @@ -46,206 +46,114 @@ export class StackGalleryAuthProvider implements plugins.smartregistry.IAuthProv } /** - * Authenticate a request and return the actor - * Called by smartregistry for every incoming request + * Authenticate with username/password credentials + * Returns userId on success, null on failure */ - public async authenticate(request: plugins.smartregistry.IAuthRequest): Promise { - const auditContext = AuditService.withContext({ - actorIp: request.ip, - actorUserAgent: request.userAgent, - }); - - // Extract auth credentials - const authHeader = request.headers?.['authorization'] || request.headers?.['Authorization']; - - // Try Bearer token (API token) - if (authHeader?.startsWith('Bearer ')) { - const token = authHeader.substring(7); - return await this.authenticateWithApiToken(token, request, auditContext); - } - - // Try Basic auth (for npm/other CLI tools) - if (authHeader?.startsWith('Basic ')) { - const credentials = authHeader.substring(6); - return await this.authenticateWithBasicAuth(credentials, request, auditContext); - } - - // Anonymous access - return this.createAnonymousActor(request); + public async authenticate( + credentials: plugins.smartregistry.ICredentials + ): Promise { + const result = await this.authService.login(credentials.username, credentials.password); + if (!result.success || !result.user) return null; + return result.user.id; } /** - * Check if actor has permission for the requested action + * Validate a token and return auth token info + */ + public async validateToken( + token: string, + protocol?: plugins.smartregistry.TRegistryProtocol + ): Promise { + // Try API token (srg_ prefix) + if (token.startsWith('srg_')) { + const result = await this.tokenService.validateToken(token); + if (!result.valid || !result.token || !result.user) return null; + + return { + type: (protocol || result.token.protocols[0] || 'npm') as plugins.smartregistry.TRegistryProtocol, + userId: result.user.id, + scopes: result.token.scopes.map((s) => + `${s.protocol}:${s.actions.join(',')}` + ), + readonly: !result.token.scopes.some((s) => + s.actions.includes('write') || s.actions.includes('*') + ), + }; + } + + // Try JWT access token + const validated = await this.authService.validateAccessToken(token); + if (!validated) return null; + + return { + type: (protocol || 'npm') as plugins.smartregistry.TRegistryProtocol, + userId: validated.user.id, + scopes: ['*'], + }; + } + + /** + * Create a new token for a user and protocol + */ + public async createToken( + userId: string, + protocol: plugins.smartregistry.TRegistryProtocol, + options?: plugins.smartregistry.ITokenOptions + ): Promise { + const result = await this.tokenService.createToken({ + userId, + name: `${protocol}-token`, + protocols: [protocol as TRegistryProtocol], + scopes: [ + { + protocol: protocol as TRegistryProtocol, + actions: options?.readonly ? ['read'] : ['read', 'write', 'delete'], + }, + ], + }); + return result.rawToken; + } + + /** + * Revoke a token + */ + public async revokeToken(token: string): Promise { + if (token.startsWith('srg_')) { + // Hash and find the token + const result = await this.tokenService.validateToken(token); + if (result.valid && result.token) { + await this.tokenService.revokeToken(result.token.id, 'provider_revoked'); + } + } + } + + /** + * Check if a token holder is authorized for a resource and action */ public async authorize( - actor: plugins.smartregistry.IRequestActor, - request: plugins.smartregistry.IAuthorizationRequest - ): Promise { - const stackActor = actor as IStackGalleryActor; + token: plugins.smartregistry.IAuthToken | null, + resource: string, + action: string + ): Promise { + // Anonymous access: only public reads + if (!token) return false; - // Anonymous users can only read public packages - if (stackActor.type === 'anonymous') { - if (request.action === 'read' && request.isPublic) { - return { allowed: true }; - } - return { - allowed: false, - reason: 'Authentication required', - statusCode: 401, - }; - } + // Parse resource string (format: "protocol:type:name" or "org/repo") + const userId = token.userId; + if (!userId) return false; - // Check protocol access - if (!stackActor.protocols.includes(request.protocol as TRegistryProtocol) && - !stackActor.protocols.includes('*' as TRegistryProtocol)) { - return { - allowed: false, - reason: `Token does not have access to ${request.protocol} protocol`, - statusCode: 403, - }; - } + // Map action + const mappedAction = this.mapAction(action); - // Map action to TAction - const action = this.mapAction(request.action); + // For simple authorization without specific resource context, + // check if user is active + const user = await User.findById(userId); + if (!user || !user.isActive) return false; - // Resolve permissions - const permissions = await this.permissionService.resolvePermissions({ - userId: stackActor.userId!, - organizationId: request.organizationId, - repositoryId: request.repositoryId, - protocol: request.protocol as TRegistryProtocol, - }); + // System admins bypass all checks + if (user.isSystemAdmin) return true; - // Check permission - let allowed = false; - switch (action) { - case 'read': - allowed = permissions.canRead || (request.isPublic ?? false); - break; - case 'write': - allowed = permissions.canWrite; - break; - case 'delete': - allowed = permissions.canDelete; - break; - case 'admin': - allowed = permissions.canAdmin; - break; - } - - if (!allowed) { - return { - allowed: false, - reason: `Insufficient permissions for ${request.action} on ${request.resourceType}`, - statusCode: 403, - }; - } - - return { allowed: true }; - } - - /** - * Authenticate using API token - */ - private async authenticateWithApiToken( - rawToken: string, - request: plugins.smartregistry.IAuthRequest, - auditContext: AuditService - ): Promise { - const result = await this.tokenService.validateToken(rawToken, request.ip); - - if (!result.valid || !result.token || !result.user) { - await auditContext.logFailure( - 'TOKEN_USED', - 'api_token', - result.errorCode || 'UNKNOWN', - result.errorMessage || 'Token validation failed' - ); - - return this.createAnonymousActor(request); - } - - await auditContext.log('TOKEN_USED', 'api_token', { - resourceId: result.token.id, - success: true, - }); - - return { - type: 'api_token', - userId: result.user.id, - user: result.user, - tokenId: result.token.id, - ip: request.ip, - userAgent: request.userAgent, - protocols: result.token.protocols, - permissions: { - canRead: true, - canWrite: true, - canDelete: true, - }, - }; - } - - /** - * Authenticate using Basic auth (username:password or username:token) - */ - private async authenticateWithBasicAuth( - credentials: string, - request: plugins.smartregistry.IAuthRequest, - auditContext: AuditService - ): Promise { - try { - const decoded = atob(credentials); - const [username, password] = decoded.split(':'); - - // If password looks like an API token, try token auth - if (password?.startsWith('srg_')) { - return await this.authenticateWithApiToken(password, request, auditContext); - } - - // Otherwise try username/password (email/password) - const result = await this.authService.login(username, password, { - userAgent: request.userAgent, - ipAddress: request.ip, - }); - - if (!result.success || !result.user) { - return this.createAnonymousActor(request); - } - - return { - type: 'user', - userId: result.user.id, - user: result.user, - ip: request.ip, - userAgent: request.userAgent, - protocols: ['npm', 'oci', 'maven', 'cargo', 'composer', 'pypi', 'rubygems'], - permissions: { - canRead: true, - canWrite: true, - canDelete: true, - }, - }; - } catch { - return this.createAnonymousActor(request); - } - } - - /** - * Create anonymous actor - */ - private createAnonymousActor(request: plugins.smartregistry.IAuthRequest): IStackGalleryActor { - return { - type: 'anonymous', - ip: request.ip, - userAgent: request.userAgent, - protocols: [], - permissions: { - canRead: false, - canWrite: false, - canDelete: false, - }, - }; + return mappedAction === 'read'; // Default: authenticated users can read } /** diff --git a/ts/providers/storage.provider.ts b/ts/providers/storage.provider.ts index 54df03d..b97a4cb 100644 --- a/ts/providers/storage.provider.ts +++ b/ts/providers/storage.provider.ts @@ -6,12 +6,12 @@ import * as plugins from '../plugins.ts'; import type { TRegistryProtocol } from '../interfaces/auth.interfaces.ts'; import { Package } from '../models/package.ts'; -import { Repository } from '../models/repository.ts'; import { Organization } from '../models/organization.ts'; import { AuditService } from '../services/audit.service.ts'; -export interface IStorageConfig { +export interface IStorageProviderConfig { bucket: plugins.smartbucket.SmartBucket; + bucketName: string; basePath: string; } @@ -20,222 +20,192 @@ export interface IStorageConfig { * and stores artifacts in S3 via smartbucket */ export class StackGalleryStorageHooks implements plugins.smartregistry.IStorageHooks { - private config: IStorageConfig; + private config: IStorageProviderConfig; - constructor(config: IStorageConfig) { + constructor(config: IStorageProviderConfig) { this.config = config; } /** * Called before a package is stored - * Use this to validate, transform, or prepare for storage */ - public async beforeStore(context: plugins.smartregistry.IStorageContext): Promise { + public async beforePut( + context: plugins.smartregistry.IStorageHookContext + ): Promise { // Validate organization exists and has quota - const org = await Organization.findById(context.organizationId); - if (!org) { - throw new Error(`Organization not found: ${context.organizationId}`); - } + const orgId = context.actor?.orgId; + if (orgId) { + const org = await Organization.findById(orgId); + if (!org) { + return { allowed: false, reason: `Organization not found: ${orgId}` }; + } - // Check storage quota - const newSize = context.size || 0; - if (org.settings.quotas.maxStorageBytes > 0) { - if (org.usedStorageBytes + newSize > org.settings.quotas.maxStorageBytes) { - throw new Error('Organization storage quota exceeded'); + // Check storage quota + const newSize = context.metadata?.size || 0; + if (!org.hasStorageAvailable(newSize)) { + return { allowed: false, reason: 'Organization storage quota exceeded' }; } } - // Validate repository exists - const repo = await Repository.findById(context.repositoryId); - if (!repo) { - throw new Error(`Repository not found: ${context.repositoryId}`); - } - - // Check repository protocol - if (!repo.protocols.includes(context.protocol as TRegistryProtocol)) { - throw new Error(`Repository does not support ${context.protocol} protocol`); - } - - return context; + return { allowed: true }; } /** * Called after a package is successfully stored - * Update database records and metrics */ - public async afterStore(context: plugins.smartregistry.IStorageContext): Promise { + public async afterPut( + context: plugins.smartregistry.IStorageHookContext + ): Promise { const protocol = context.protocol as TRegistryProtocol; - const packageId = Package.generateId(protocol, context.organizationName, context.packageName); + const packageName = context.metadata?.packageName || context.key; + const version = context.metadata?.version || 'unknown'; + const orgId = context.actor?.orgId || ''; + + const packageId = Package.generateId(protocol, orgId, packageName); // Get or create package record let pkg = await Package.findById(packageId); if (!pkg) { pkg = new Package(); pkg.id = packageId; - pkg.organizationId = context.organizationId; - pkg.repositoryId = context.repositoryId; + pkg.organizationId = orgId; pkg.protocol = protocol; - pkg.name = context.packageName; - pkg.createdById = context.actorId || ''; + pkg.name = packageName; + pkg.createdById = context.actor?.userId || ''; pkg.createdAt = new Date(); } // Add version pkg.addVersion({ - version: context.version, + version, publishedAt: new Date(), - publishedBy: context.actorId || '', - size: context.size || 0, - checksum: context.checksum || '', - checksumAlgorithm: context.checksumAlgorithm || 'sha256', + publishedById: context.actor?.userId || '', + size: context.metadata?.size || 0, + digest: context.metadata?.digest, downloads: 0, - metadata: context.metadata || {}, + metadata: {}, }); - // Update dist tags if provided - if (context.tags) { - for (const [tag, version] of Object.entries(context.tags)) { - pkg.distTags[tag] = version; - } - } - // Set latest tag if not set if (!pkg.distTags['latest']) { - pkg.distTags['latest'] = context.version; + pkg.distTags['latest'] = version; } await pkg.save(); // Update organization storage usage - const org = await Organization.findById(context.organizationId); - if (org) { - org.usedStorageBytes += context.size || 0; - await org.save(); + if (orgId) { + const org = await Organization.findById(orgId); + if (org) { + await org.updateStorageUsage(context.metadata?.size || 0); + } } // Audit log - await AuditService.withContext({ - actorId: context.actorId, - actorType: context.actorId ? 'user' : 'anonymous', - organizationId: context.organizationId, - repositoryId: context.repositoryId, - }).logPackagePublished( - packageId, - context.packageName, - context.version, - context.organizationId, - context.repositoryId - ); - } - - /** - * Called before a package is fetched - */ - public async beforeFetch(context: plugins.smartregistry.IFetchContext): Promise { - return context; + if (context.actor?.userId) { + await AuditService.withContext({ + actorId: context.actor.userId, + actorType: 'user', + organizationId: orgId, + }).logPackagePublished(packageId, packageName, version, orgId, ''); + } } /** * Called after a package is fetched - * Update download metrics */ - public async afterFetch(context: plugins.smartregistry.IFetchContext): Promise { + public async afterGet( + context: plugins.smartregistry.IStorageHookContext + ): Promise { const protocol = context.protocol as TRegistryProtocol; - const packageId = Package.generateId(protocol, context.organizationName, context.packageName); + const packageName = context.metadata?.packageName || context.key; + const version = context.metadata?.version; + const orgId = context.actor?.orgId || ''; + + const packageId = Package.generateId(protocol, orgId, packageName); const pkg = await Package.findById(packageId); if (pkg) { - await pkg.incrementDownloads(context.version); - } - - // Audit log for authenticated users - if (context.actorId) { - await AuditService.withContext({ - actorId: context.actorId, - actorType: 'user', - organizationId: context.organizationId, - repositoryId: context.repositoryId, - }).logPackageDownloaded( - packageId, - context.packageName, - context.version || 'latest', - context.organizationId, - context.repositoryId - ); + await pkg.incrementDownloads(version); } } /** * Called before a package is deleted */ - public async beforeDelete(context: plugins.smartregistry.IDeleteContext): Promise { - return context; + public async beforeDelete( + context: plugins.smartregistry.IStorageHookContext + ): Promise { + return { allowed: true }; } /** * Called after a package is deleted */ - public async afterDelete(context: plugins.smartregistry.IDeleteContext): Promise { + public async afterDelete( + context: plugins.smartregistry.IStorageHookContext + ): Promise { const protocol = context.protocol as TRegistryProtocol; - const packageId = Package.generateId(protocol, context.organizationName, context.packageName); + const packageName = context.metadata?.packageName || context.key; + const version = context.metadata?.version; + const orgId = context.actor?.orgId || ''; + + const packageId = Package.generateId(protocol, orgId, packageName); const pkg = await Package.findById(packageId); if (!pkg) return; - if (context.version) { - // Delete specific version - const version = pkg.versions[context.version]; - if (version) { - const sizeReduction = version.size; - delete pkg.versions[context.version]; + if (version) { + const versionData = pkg.versions[version]; + if (versionData) { + const sizeReduction = versionData.size; + delete pkg.versions[version]; pkg.storageBytes -= sizeReduction; - // Update dist tags for (const [tag, ver] of Object.entries(pkg.distTags)) { - if (ver === context.version) { + if (ver === version) { delete pkg.distTags[tag]; } } - // If no versions left, delete the package if (Object.keys(pkg.versions).length === 0) { await pkg.delete(); } else { await pkg.save(); } - // Update org storage - const org = await Organization.findById(context.organizationId); - if (org) { - org.usedStorageBytes -= sizeReduction; - await org.save(); + if (orgId) { + const org = await Organization.findById(orgId); + if (org) { + await org.updateStorageUsage(-sizeReduction); + } } } } else { - // Delete entire package const sizeReduction = pkg.storageBytes; await pkg.delete(); - // Update org storage - const org = await Organization.findById(context.organizationId); - if (org) { - org.usedStorageBytes -= sizeReduction; - await org.save(); + if (orgId) { + const org = await Organization.findById(orgId); + if (org) { + await org.updateStorageUsage(-sizeReduction); + } } } // Audit log - await AuditService.withContext({ - actorId: context.actorId, - actorType: context.actorId ? 'user' : 'system', - organizationId: context.organizationId, - repositoryId: context.repositoryId, - }).log('PACKAGE_DELETED', 'package', { - resourceId: packageId, - resourceName: context.packageName, - metadata: { version: context.version }, - success: true, - }); + if (context.actor?.userId) { + await AuditService.withContext({ + actorId: context.actor.userId, + actorType: 'user', + organizationId: orgId, + }).log('PACKAGE_DELETED', 'package', { + resourceId: packageId, + resourceName: packageName, + metadata: { version }, + success: true, + }); + } } /** @@ -259,11 +229,10 @@ export class StackGalleryStorageHooks implements plugins.smartregistry.IStorageH data: Uint8Array, contentType?: string ): Promise { - const bucket = await this.config.bucket.getBucket(); + const bucket = await this.config.bucket.getBucketByName(this.config.bucketName); await bucket.fastPut({ path, - contents: Buffer.from(data), - contentType: contentType || 'application/octet-stream', + contents: data as unknown as string, }); return path; } @@ -273,10 +242,10 @@ export class StackGalleryStorageHooks implements plugins.smartregistry.IStorageH */ public async fetchArtifact(path: string): Promise { try { - const bucket = await this.config.bucket.getBucket(); + const bucket = await this.config.bucket.getBucketByName(this.config.bucketName); const file = await bucket.fastGet({ path }); if (!file) return null; - return new Uint8Array(file.contents); + return new Uint8Array(file); } catch { return null; } @@ -287,8 +256,8 @@ export class StackGalleryStorageHooks implements plugins.smartregistry.IStorageH */ public async deleteArtifact(path: string): Promise { try { - const bucket = await this.config.bucket.getBucket(); - await bucket.fastDelete({ path }); + const bucket = await this.config.bucket.getBucketByName(this.config.bucketName); + await bucket.fastRemove({ path }); return true; } catch { return false; diff --git a/ts/registry.ts b/ts/registry.ts index 73bb8e6..aab6bc9 100644 --- a/ts/registry.ts +++ b/ts/registry.ts @@ -86,6 +86,7 @@ export class StackGalleryRegistry { // Initialize storage hooks this.storageHooks = new StackGalleryStorageHooks({ bucket: this.smartBucket, + bucketName: this.config.s3Bucket, basePath: this.config.storagePath!, }); @@ -95,16 +96,22 @@ export class StackGalleryRegistry { authProvider: this.authProvider, storageHooks: this.storageHooks, storage: { - type: 's3', - bucket: this.smartBucket, - basePath: this.config.storagePath, + endpoint: this.config.s3Endpoint, + accessKey: this.config.s3AccessKey, + accessSecret: this.config.s3SecretKey, + bucketName: this.config.s3Bucket, + region: this.config.s3Region, + }, + auth: { + jwtSecret: this.config.jwtSecret || 'change-me-in-production', + tokenStore: 'database', + npmTokens: { enabled: true }, + ociTokens: { + enabled: true, + realm: 'stack.gallery', + service: 'registry', + }, }, - upstreamCache: this.config.enableUpstreamCache - ? { - enabled: true, - expiryHours: this.config.upstreamCacheExpiry, - } - : undefined, }); console.log('[StackGalleryRegistry] smartregistry initialized'); @@ -161,30 +168,34 @@ export class StackGalleryRegistry { } // Registry protocol endpoints (handled by smartregistry) - // NPM: /-/..., /@scope/package (but not /packages which is UI route) - // OCI: /v2/... - // Maven: /maven2/... - // PyPI: /simple/..., /pypi/... - // Cargo: /api/v1/crates/... - // Composer: /packages.json, /p/... - // RubyGems: /api/v1/gems/..., /gems/... - const registryPaths = ['/-/', '/v2/', '/maven2/', '/simple/', '/pypi/', '/api/v1/crates/', '/packages.json', '/p/', '/api/v1/gems/', '/gems/']; - const isRegistryPath = registryPaths.some(p => path.startsWith(p)) || - (path.startsWith('/@') && !path.startsWith('/@stack')); + const registryPaths = [ + '/-/', + '/v2/', + '/maven2/', + '/simple/', + '/pypi/', + '/api/v1/crates/', + '/packages.json', + '/p/', + '/api/v1/gems/', + '/gems/', + ]; + const isRegistryPath = + registryPaths.some((p) => path.startsWith(p)) || + (path.startsWith('/@') && !path.startsWith('/@stack')); if (this.smartRegistry && isRegistryPath) { try { - const response = await this.smartRegistry.handleRequest(request); - if (response) return response; + // Convert Request to IRequestContext + const requestContext = await this.requestToContext(request); + const response = await this.smartRegistry.handleRequest(requestContext); + if (response) return this.contextResponseToResponse(response); } catch (error) { console.error('[StackGalleryRegistry] Request error:', error); - return new Response( - JSON.stringify({ error: 'Internal server error' }), - { - status: 500, - headers: { 'Content-Type': 'application/json' }, - } - ); + return new Response(JSON.stringify({ error: 'Internal server error' }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); } } @@ -197,6 +208,82 @@ export class StackGalleryRegistry { return this.serveStaticFile(path); } + /** + * Convert a Deno Request to smartregistry IRequestContext + */ + private async requestToContext( + request: Request + ): Promise { + const url = new URL(request.url); + const headers: Record = {}; + request.headers.forEach((value, key) => { + headers[key] = value; + }); + const query: Record = {}; + url.searchParams.forEach((value, key) => { + query[key] = value; + }); + + let body: unknown = undefined; + // deno-lint-ignore no-explicit-any + let rawBody: any = undefined; + if (request.body && request.method !== 'GET' && request.method !== 'HEAD') { + try { + const bytes = new Uint8Array(await request.arrayBuffer()); + rawBody = bytes; + const contentType = request.headers.get('content-type') || ''; + if (contentType.includes('json')) { + body = JSON.parse(new TextDecoder().decode(bytes)); + } + } catch { + // Body parsing failed, continue with undefined body + } + } + + // Extract token from Authorization header + let token: string | undefined; + const authHeader = headers['authorization']; + if (authHeader?.startsWith('Bearer ')) { + token = authHeader.substring(7); + } + + return { + method: request.method, + path: url.pathname, + headers, + query, + body, + rawBody, + token, + }; + } + + /** + * Convert smartregistry IResponse to Deno Response + */ + private contextResponseToResponse(response: plugins.smartregistry.IResponse): Response { + const headers = new Headers(response.headers || {}); + let body: BodyInit | null = null; + + if (response.body !== undefined) { + if (typeof response.body === 'string') { + body = response.body; + } else if (response.body instanceof Uint8Array) { + body = response.body as unknown as BodyInit; + } else { + body = JSON.stringify(response.body); + if (!headers.has('content-type')) { + headers.set('content-type', 'application/json'); + } + } + } + + return new Response(body, { + status: response.status, + headers, + }); + } + /** * Serve static files from embedded UI */ @@ -206,7 +293,7 @@ export class StackGalleryRegistry { // Get embedded file const embeddedFile = getEmbeddedFile(filePath); if (embeddedFile) { - return new Response(embeddedFile.data, { + return new Response(embeddedFile.data as unknown as BodyInit, { status: 200, headers: { 'Content-Type': embeddedFile.contentType }, }); @@ -215,7 +302,7 @@ export class StackGalleryRegistry { // SPA fallback: serve index.html for unknown paths const indexFile = getEmbeddedFile('/index.html'); if (indexFile) { - return new Response(indexFile.data, { + return new Response(indexFile.data as unknown as BodyInit, { status: 200, headers: { 'Content-Type': 'text/html' }, }); @@ -229,13 +316,10 @@ export class StackGalleryRegistry { */ private async handleApiRequest(request: Request): Promise { if (!this.apiRouter) { - return new Response( - JSON.stringify({ error: 'API router not initialized' }), - { - status: 503, - headers: { 'Content-Type': 'application/json' }, - } - ); + return new Response(JSON.stringify({ error: 'API router not initialized' }), { + status: 503, + headers: { 'Content-Type': 'application/json' }, + }); } return await this.apiRouter.handle(request); @@ -336,7 +420,9 @@ export async function createRegistryFromEnvFile(): Promise const s3Endpoint = `${s3Protocol}://${env.S3_HOST || 'localhost'}:${env.S3_PORT || '9000'}`; const config: IRegistryConfig = { - mongoUrl: env.MONGODB_URL || `mongodb://${env.MONGODB_USER}:${env.MONGODB_PASS}@${env.MONGODB_HOST || 'localhost'}:${env.MONGODB_PORT || '27017'}/${env.MONGODB_NAME}?authSource=admin`, + mongoUrl: + env.MONGODB_URL || + `mongodb://${env.MONGODB_USER}:${env.MONGODB_PASS}@${env.MONGODB_HOST || 'localhost'}:${env.MONGODB_PORT || '27017'}/${env.MONGODB_NAME}?authSource=admin`, mongoDb: env.MONGODB_NAME || 'stackgallery', s3Endpoint: s3Endpoint, s3AccessKey: env.S3_ACCESSKEY || env.S3_ACCESS_KEY || 'minioadmin', @@ -356,7 +442,10 @@ export async function createRegistryFromEnvFile(): Promise if (error instanceof Deno.errors.NotFound) { console.log('[StackGalleryRegistry] No .nogit/env.json found, using environment variables'); } else { - console.warn('[StackGalleryRegistry] Error reading .nogit/env.json, falling back to env vars:', error); + console.warn( + '[StackGalleryRegistry] Error reading .nogit/env.json, falling back to env vars:', + error + ); } return createRegistryFromEnv(); } diff --git a/ts/services/crypto.service.ts b/ts/services/crypto.service.ts index 0b6c719..9b0d2d5 100644 --- a/ts/services/crypto.service.ts +++ b/ts/services/crypto.service.ts @@ -50,9 +50,9 @@ export class CryptoService { // Encrypt const encrypted = await crypto.subtle.encrypt( - { name: 'AES-GCM', iv }, + { name: 'AES-GCM', iv: iv.buffer as ArrayBuffer }, this.masterKey, - encoded + encoded.buffer as ArrayBuffer ); // Format: iv:ciphertext (both base64) @@ -86,9 +86,9 @@ export class CryptoService { // Decrypt const decrypted = await crypto.subtle.decrypt( - { name: 'AES-GCM', iv }, + { name: 'AES-GCM', iv: iv.buffer as ArrayBuffer }, this.masterKey, - encrypted + encrypted.buffer as ArrayBuffer ); // Decode to string @@ -120,7 +120,7 @@ export class CryptoService { const keyBytes = this.hexToBytes(keyHex); return await crypto.subtle.importKey( 'raw', - keyBytes, + keyBytes.buffer as ArrayBuffer, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt'] diff --git a/ts/services/external.auth.service.ts b/ts/services/external.auth.service.ts index c46f68f..0c90007 100644 --- a/ts/services/external.auth.service.ts +++ b/ts/services/external.auth.service.ts @@ -103,7 +103,7 @@ export class ExternalAuthService { try { externalUser = await strategy.handleCallback(data); } catch (error) { - await this.auditService.log('USER_LOGIN', 'user', { + await this.auditService.log('AUTH_LOGIN', 'user', { success: false, metadata: { providerId: provider.id, @@ -143,7 +143,7 @@ export class ExternalAuthService { actorType: 'user', actorIp: options.ipAddress, actorUserAgent: options.userAgent, - }).log('USER_LOGIN', 'user', { + }).log('AUTH_LOGIN', 'user', { resourceId: user.id, success: true, metadata: { @@ -194,7 +194,7 @@ export class ExternalAuthService { try { externalUser = await strategy.authenticateCredentials(username, password); } catch (error) { - await this.auditService.log('USER_LOGIN', 'user', { + await this.auditService.log('AUTH_LOGIN', 'user', { success: false, metadata: { providerId: provider.id, @@ -235,7 +235,7 @@ export class ExternalAuthService { actorType: 'user', actorIp: options.ipAddress, actorUserAgent: options.userAgent, - }).log('USER_LOGIN', 'user', { + }).log('AUTH_LOGIN', 'user', { resourceId: user.id, success: true, metadata: {