@push.rocks/smartregistry 2.2.3 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/cargo/classes.cargoregistry.d.ts +7 -1
- package/dist_ts/cargo/classes.cargoregistry.js +42 -4
- package/dist_ts/cargo/classes.cargoupstream.d.ts +44 -0
- package/dist_ts/cargo/classes.cargoupstream.js +129 -0
- package/dist_ts/cargo/index.d.ts +1 -0
- package/dist_ts/cargo/index.js +2 -1
- package/dist_ts/classes.smartregistry.d.ts +33 -2
- package/dist_ts/classes.smartregistry.js +45 -12
- package/dist_ts/composer/classes.composerregistry.d.ts +7 -1
- package/dist_ts/composer/classes.composerregistry.js +34 -3
- package/dist_ts/composer/classes.composerupstream.d.ts +40 -0
- package/dist_ts/composer/classes.composerupstream.js +159 -0
- package/dist_ts/composer/index.d.ts +1 -0
- package/dist_ts/composer/index.js +2 -1
- package/dist_ts/core/classes.authmanager.d.ts +30 -80
- package/dist_ts/core/classes.authmanager.js +63 -337
- package/dist_ts/core/classes.defaultauthprovider.d.ts +78 -0
- package/dist_ts/core/classes.defaultauthprovider.js +311 -0
- package/dist_ts/core/classes.registrystorage.d.ts +70 -4
- package/dist_ts/core/classes.registrystorage.js +165 -5
- package/dist_ts/core/index.d.ts +3 -0
- package/dist_ts/core/index.js +7 -2
- package/dist_ts/core/interfaces.auth.d.ts +83 -0
- package/dist_ts/core/interfaces.auth.js +2 -0
- package/dist_ts/core/interfaces.core.d.ts +38 -0
- package/dist_ts/core/interfaces.storage.d.ts +120 -0
- package/dist_ts/core/interfaces.storage.js +2 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/maven/classes.mavenregistry.d.ts +12 -1
- package/dist_ts/maven/classes.mavenregistry.js +69 -4
- package/dist_ts/maven/classes.mavenupstream.d.ts +45 -0
- package/dist_ts/maven/classes.mavenupstream.js +153 -0
- package/dist_ts/maven/index.d.ts +1 -0
- package/dist_ts/maven/index.js +2 -1
- package/dist_ts/npm/classes.npmregistry.d.ts +3 -1
- package/dist_ts/npm/classes.npmregistry.js +55 -6
- package/dist_ts/npm/classes.npmupstream.d.ts +51 -0
- package/dist_ts/npm/classes.npmupstream.js +206 -0
- package/dist_ts/npm/index.d.ts +1 -0
- package/dist_ts/npm/index.js +2 -1
- package/dist_ts/oci/classes.ociregistry.d.ts +4 -1
- package/dist_ts/oci/classes.ociregistry.js +78 -17
- package/dist_ts/oci/classes.ociupstream.d.ts +62 -0
- package/dist_ts/oci/classes.ociupstream.js +206 -0
- package/dist_ts/oci/index.d.ts +1 -0
- package/dist_ts/oci/index.js +2 -1
- package/dist_ts/plugins.d.ts +4 -1
- package/dist_ts/plugins.js +6 -2
- package/dist_ts/pypi/classes.pypiregistry.d.ts +7 -1
- package/dist_ts/pypi/classes.pypiregistry.js +60 -4
- package/dist_ts/pypi/classes.pypiupstream.d.ts +48 -0
- package/dist_ts/pypi/classes.pypiupstream.js +165 -0
- package/dist_ts/pypi/index.d.ts +1 -0
- package/dist_ts/pypi/index.js +2 -1
- package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +7 -1
- package/dist_ts/rubygems/classes.rubygemsregistry.js +35 -4
- package/dist_ts/rubygems/classes.rubygemsupstream.d.ts +47 -0
- package/dist_ts/rubygems/classes.rubygemsupstream.js +184 -0
- package/dist_ts/rubygems/index.d.ts +1 -0
- package/dist_ts/rubygems/index.js +2 -1
- package/dist_ts/upstream/classes.baseupstream.d.ts +112 -0
- package/dist_ts/upstream/classes.baseupstream.js +411 -0
- package/dist_ts/upstream/classes.circuitbreaker.d.ts +111 -0
- package/dist_ts/upstream/classes.circuitbreaker.js +192 -0
- package/dist_ts/upstream/classes.upstreamcache.d.ts +170 -0
- package/dist_ts/upstream/classes.upstreamcache.js +485 -0
- package/dist_ts/upstream/index.d.ts +6 -0
- package/dist_ts/upstream/index.js +7 -0
- package/dist_ts/upstream/interfaces.upstream.d.ts +169 -0
- package/dist_ts/upstream/interfaces.upstream.js +23 -0
- package/package.json +4 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/cargo/classes.cargoregistry.ts +48 -3
- package/ts/cargo/classes.cargoupstream.ts +159 -0
- package/ts/cargo/index.ts +1 -0
- package/ts/classes.smartregistry.ts +88 -11
- package/ts/composer/classes.composerregistry.ts +39 -2
- package/ts/composer/classes.composerupstream.ts +200 -0
- package/ts/composer/index.ts +1 -0
- package/ts/core/classes.authmanager.ts +74 -412
- package/ts/core/classes.defaultauthprovider.ts +393 -0
- package/ts/core/classes.registrystorage.ts +199 -5
- package/ts/core/index.ts +8 -1
- package/ts/core/interfaces.auth.ts +91 -0
- package/ts/core/interfaces.core.ts +42 -0
- package/ts/core/interfaces.storage.ts +130 -0
- package/ts/index.ts +3 -0
- package/ts/maven/classes.mavenregistry.ts +84 -3
- package/ts/maven/classes.mavenupstream.ts +220 -0
- package/ts/maven/index.ts +1 -0
- package/ts/npm/classes.npmregistry.ts +61 -5
- package/ts/npm/classes.npmupstream.ts +260 -0
- package/ts/npm/index.ts +1 -0
- package/ts/oci/classes.ociregistry.ts +89 -17
- package/ts/oci/classes.ociupstream.ts +263 -0
- package/ts/oci/index.ts +1 -0
- package/ts/plugins.ts +7 -1
- package/ts/pypi/classes.pypiregistry.ts +68 -3
- package/ts/pypi/classes.pypiupstream.ts +211 -0
- package/ts/pypi/index.ts +1 -0
- package/ts/rubygems/classes.rubygemsregistry.ts +40 -3
- package/ts/rubygems/classes.rubygemsupstream.ts +230 -0
- package/ts/rubygems/index.ts +1 -0
- package/ts/upstream/classes.baseupstream.ts +526 -0
- package/ts/upstream/classes.circuitbreaker.ts +238 -0
- package/ts/upstream/classes.upstreamcache.ts +626 -0
- package/ts/upstream/index.ts +11 -0
- package/ts/upstream/interfaces.upstream.ts +195 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import type { IAuthProvider, ITokenOptions } from './interfaces.auth.js';
|
|
3
|
+
import type { IAuthConfig, IAuthToken, ICredentials, TRegistryProtocol } from './interfaces.core.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default in-memory authentication provider.
|
|
7
|
+
* This is the reference implementation that stores tokens in memory.
|
|
8
|
+
* For production use, implement IAuthProvider with Redis, database, or external auth.
|
|
9
|
+
*/
|
|
10
|
+
export class DefaultAuthProvider implements IAuthProvider {
|
|
11
|
+
private tokenStore: Map<string, IAuthToken> = new Map();
|
|
12
|
+
private userCredentials: Map<string, string> = new Map(); // username -> password hash (mock)
|
|
13
|
+
|
|
14
|
+
constructor(private config: IAuthConfig) {}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize the auth provider
|
|
18
|
+
*/
|
|
19
|
+
public async init(): Promise<void> {
|
|
20
|
+
// Initialize token store (in-memory for now)
|
|
21
|
+
// In production, this could be Redis or a database
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ========================================================================
|
|
25
|
+
// IAuthProvider Implementation
|
|
26
|
+
// ========================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Authenticate user credentials
|
|
30
|
+
*/
|
|
31
|
+
public async authenticate(credentials: ICredentials): Promise<string | null> {
|
|
32
|
+
// Mock authentication - in production, verify against database/LDAP
|
|
33
|
+
const storedPassword = this.userCredentials.get(credentials.username);
|
|
34
|
+
|
|
35
|
+
if (!storedPassword) {
|
|
36
|
+
// Auto-register for testing (remove in production)
|
|
37
|
+
this.userCredentials.set(credentials.username, credentials.password);
|
|
38
|
+
return credentials.username;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (storedPassword === credentials.password) {
|
|
42
|
+
return credentials.username;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validate any token (NPM, Maven, OCI, PyPI, RubyGems, Composer, Cargo)
|
|
50
|
+
*/
|
|
51
|
+
public async validateToken(
|
|
52
|
+
tokenString: string,
|
|
53
|
+
protocol?: TRegistryProtocol
|
|
54
|
+
): Promise<IAuthToken | null> {
|
|
55
|
+
// OCI uses JWT (contains dots), not UUID - check first if OCI is expected
|
|
56
|
+
if (protocol === 'oci' || tokenString.includes('.')) {
|
|
57
|
+
const ociToken = await this.validateOciToken(tokenString);
|
|
58
|
+
if (ociToken && (!protocol || protocol === 'oci')) {
|
|
59
|
+
return ociToken;
|
|
60
|
+
}
|
|
61
|
+
// If protocol was explicitly OCI but validation failed, return null
|
|
62
|
+
if (protocol === 'oci') {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// UUID-based tokens: single O(1) Map lookup
|
|
68
|
+
if (this.isValidUuid(tokenString)) {
|
|
69
|
+
const authToken = this.tokenStore.get(tokenString);
|
|
70
|
+
if (authToken) {
|
|
71
|
+
// If protocol specified, verify it matches
|
|
72
|
+
if (protocol && authToken.type !== protocol) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
// Check expiration
|
|
76
|
+
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
|
|
77
|
+
this.tokenStore.delete(tokenString);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return authToken;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create a new token for a user
|
|
89
|
+
*/
|
|
90
|
+
public async createToken(
|
|
91
|
+
userId: string,
|
|
92
|
+
protocol: TRegistryProtocol,
|
|
93
|
+
options?: ITokenOptions
|
|
94
|
+
): Promise<string> {
|
|
95
|
+
// OCI tokens use JWT
|
|
96
|
+
if (protocol === 'oci') {
|
|
97
|
+
return this.createOciToken(userId, options?.scopes || ['oci:*:*:*'], options?.expiresIn || 3600);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// All other protocols use UUID tokens
|
|
101
|
+
const token = this.generateUuid();
|
|
102
|
+
const scopes = options?.scopes || (options?.readonly
|
|
103
|
+
? [`${protocol}:*:*:read`]
|
|
104
|
+
: [`${protocol}:*:*:*`]);
|
|
105
|
+
|
|
106
|
+
const authToken: IAuthToken = {
|
|
107
|
+
type: protocol,
|
|
108
|
+
userId,
|
|
109
|
+
scopes,
|
|
110
|
+
readonly: options?.readonly,
|
|
111
|
+
expiresAt: options?.expiresIn ? new Date(Date.now() + options.expiresIn * 1000) : undefined,
|
|
112
|
+
metadata: {
|
|
113
|
+
created: new Date().toISOString(),
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
this.tokenStore.set(token, authToken);
|
|
118
|
+
return token;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Revoke a token
|
|
123
|
+
*/
|
|
124
|
+
public async revokeToken(token: string): Promise<void> {
|
|
125
|
+
this.tokenStore.delete(token);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if token has permission for an action
|
|
130
|
+
*/
|
|
131
|
+
public async authorize(
|
|
132
|
+
token: IAuthToken | null,
|
|
133
|
+
resource: string,
|
|
134
|
+
action: string
|
|
135
|
+
): Promise<boolean> {
|
|
136
|
+
if (!token) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check readonly flag
|
|
141
|
+
if (token.readonly && ['write', 'push', 'delete'].includes(action)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check scopes
|
|
146
|
+
for (const scope of token.scopes) {
|
|
147
|
+
if (this.matchesScope(scope, resource, action)) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* List all tokens for a user
|
|
157
|
+
*/
|
|
158
|
+
public async listUserTokens(userId: string): Promise<Array<{
|
|
159
|
+
key: string;
|
|
160
|
+
readonly: boolean;
|
|
161
|
+
created: string;
|
|
162
|
+
protocol?: TRegistryProtocol;
|
|
163
|
+
}>> {
|
|
164
|
+
const tokens: Array<{key: string; readonly: boolean; created: string; protocol?: TRegistryProtocol}> = [];
|
|
165
|
+
|
|
166
|
+
for (const [token, authToken] of this.tokenStore.entries()) {
|
|
167
|
+
if (authToken.userId === userId) {
|
|
168
|
+
tokens.push({
|
|
169
|
+
key: this.hashToken(token),
|
|
170
|
+
readonly: authToken.readonly || false,
|
|
171
|
+
created: authToken.metadata?.created || 'unknown',
|
|
172
|
+
protocol: authToken.type,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return tokens;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ========================================================================
|
|
181
|
+
// OCI JWT Token Methods
|
|
182
|
+
// ========================================================================
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create an OCI JWT token
|
|
186
|
+
*/
|
|
187
|
+
private async createOciToken(
|
|
188
|
+
userId: string,
|
|
189
|
+
scopes: string[],
|
|
190
|
+
expiresIn: number = 3600
|
|
191
|
+
): Promise<string> {
|
|
192
|
+
if (!this.config.ociTokens?.enabled) {
|
|
193
|
+
throw new Error('OCI tokens are not enabled');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const now = Math.floor(Date.now() / 1000);
|
|
197
|
+
const payload = {
|
|
198
|
+
iss: this.config.ociTokens.realm,
|
|
199
|
+
sub: userId,
|
|
200
|
+
aud: this.config.ociTokens.service,
|
|
201
|
+
exp: now + expiresIn,
|
|
202
|
+
nbf: now,
|
|
203
|
+
iat: now,
|
|
204
|
+
access: this.scopesToOciAccess(scopes),
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Create JWT with HMAC-SHA256 signature
|
|
208
|
+
const header = { alg: 'HS256', typ: 'JWT' };
|
|
209
|
+
const headerB64 = Buffer.from(JSON.stringify(header)).toString('base64url');
|
|
210
|
+
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
211
|
+
|
|
212
|
+
const signature = crypto
|
|
213
|
+
.createHmac('sha256', this.config.jwtSecret)
|
|
214
|
+
.update(`${headerB64}.${payloadB64}`)
|
|
215
|
+
.digest('base64url');
|
|
216
|
+
|
|
217
|
+
return `${headerB64}.${payloadB64}.${signature}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Validate an OCI JWT token
|
|
222
|
+
*/
|
|
223
|
+
private async validateOciToken(jwt: string): Promise<IAuthToken | null> {
|
|
224
|
+
try {
|
|
225
|
+
const parts = jwt.split('.');
|
|
226
|
+
if (parts.length !== 3) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
231
|
+
|
|
232
|
+
// Verify signature
|
|
233
|
+
const expectedSignature = crypto
|
|
234
|
+
.createHmac('sha256', this.config.jwtSecret)
|
|
235
|
+
.update(`${headerB64}.${payloadB64}`)
|
|
236
|
+
.digest('base64url');
|
|
237
|
+
|
|
238
|
+
if (signatureB64 !== expectedSignature) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Decode and parse payload
|
|
243
|
+
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf-8'));
|
|
244
|
+
|
|
245
|
+
// Check expiration
|
|
246
|
+
const now = Math.floor(Date.now() / 1000);
|
|
247
|
+
if (payload.exp && payload.exp < now) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check not-before time
|
|
252
|
+
if (payload.nbf && payload.nbf > now) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Convert to unified token format
|
|
257
|
+
const scopes = this.ociAccessToScopes(payload.access || []);
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
type: 'oci',
|
|
261
|
+
userId: payload.sub,
|
|
262
|
+
scopes,
|
|
263
|
+
expiresAt: payload.exp ? new Date(payload.exp * 1000) : undefined,
|
|
264
|
+
metadata: {
|
|
265
|
+
iss: payload.iss,
|
|
266
|
+
aud: payload.aud,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
} catch (error) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ========================================================================
|
|
275
|
+
// Helper Methods
|
|
276
|
+
// ========================================================================
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Check if a scope matches a resource and action
|
|
280
|
+
*/
|
|
281
|
+
private matchesScope(scope: string, resource: string, action: string): boolean {
|
|
282
|
+
const scopeParts = scope.split(':');
|
|
283
|
+
const resourceParts = resource.split(':');
|
|
284
|
+
|
|
285
|
+
// Scope must have at least protocol:type:name:action
|
|
286
|
+
if (scopeParts.length < 4) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const [scopeProtocol, scopeType, scopeName, scopeAction] = scopeParts;
|
|
291
|
+
const [resourceProtocol, resourceType, resourceName] = resourceParts;
|
|
292
|
+
|
|
293
|
+
// Check protocol
|
|
294
|
+
if (scopeProtocol !== '*' && scopeProtocol !== resourceProtocol) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check type
|
|
299
|
+
if (scopeType !== '*' && scopeType !== resourceType) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check name
|
|
304
|
+
if (scopeName !== '*' && scopeName !== resourceName) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Check action
|
|
309
|
+
if (scopeAction !== '*' && scopeAction !== action) {
|
|
310
|
+
// Map action aliases
|
|
311
|
+
const actionAliases: Record<string, string[]> = {
|
|
312
|
+
read: ['pull', 'get'],
|
|
313
|
+
write: ['push', 'put', 'post'],
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const aliases = actionAliases[scopeAction] || [];
|
|
317
|
+
if (!aliases.includes(action)) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Convert unified scopes to OCI access array
|
|
327
|
+
*/
|
|
328
|
+
private scopesToOciAccess(scopes: string[]): Array<{
|
|
329
|
+
type: string;
|
|
330
|
+
name: string;
|
|
331
|
+
actions: string[];
|
|
332
|
+
}> {
|
|
333
|
+
const access: Array<{type: string; name: string; actions: string[]}> = [];
|
|
334
|
+
|
|
335
|
+
for (const scope of scopes) {
|
|
336
|
+
const parts = scope.split(':');
|
|
337
|
+
if (parts.length >= 4 && parts[0] === 'oci') {
|
|
338
|
+
access.push({
|
|
339
|
+
type: parts[1],
|
|
340
|
+
name: parts[2],
|
|
341
|
+
actions: [parts[3]],
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return access;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Convert OCI access array to unified scopes
|
|
351
|
+
*/
|
|
352
|
+
private ociAccessToScopes(access: Array<{
|
|
353
|
+
type: string;
|
|
354
|
+
name: string;
|
|
355
|
+
actions: string[];
|
|
356
|
+
}>): string[] {
|
|
357
|
+
const scopes: string[] = [];
|
|
358
|
+
|
|
359
|
+
for (const item of access) {
|
|
360
|
+
for (const action of item.actions) {
|
|
361
|
+
scopes.push(`oci:${item.type}:${item.name}:${action}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return scopes;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Generate UUID for tokens
|
|
370
|
+
*/
|
|
371
|
+
private generateUuid(): string {
|
|
372
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
373
|
+
const r = (Math.random() * 16) | 0;
|
|
374
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
375
|
+
return v.toString(16);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Check if string is a valid UUID
|
|
381
|
+
*/
|
|
382
|
+
private isValidUuid(str: string): boolean {
|
|
383
|
+
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;
|
|
384
|
+
return uuidRegex.test(str);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Hash a token for identification
|
|
389
|
+
*/
|
|
390
|
+
private hashToken(token: string): string {
|
|
391
|
+
return `sha512-${token.substring(0, 16)}...`;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
@@ -1,17 +1,54 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
-
import type { IStorageConfig, IStorageBackend } from './interfaces.core.js';
|
|
2
|
+
import type { IStorageConfig, IStorageBackend, TRegistryProtocol } from './interfaces.core.js';
|
|
3
|
+
import type {
|
|
4
|
+
IStorageHooks,
|
|
5
|
+
IStorageHookContext,
|
|
6
|
+
IStorageActor,
|
|
7
|
+
IStorageMetadata,
|
|
8
|
+
} from './interfaces.storage.js';
|
|
3
9
|
|
|
4
10
|
/**
|
|
5
|
-
* Storage abstraction layer for registry
|
|
6
|
-
* Provides a unified interface over SmartBucket
|
|
11
|
+
* Storage abstraction layer for registry.
|
|
12
|
+
* Provides a unified interface over SmartBucket with optional hooks
|
|
13
|
+
* for quota tracking, audit logging, cache invalidation, etc.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* // Basic usage
|
|
18
|
+
* const storage = new RegistryStorage(config);
|
|
19
|
+
*
|
|
20
|
+
* // With hooks for quota tracking
|
|
21
|
+
* const storage = new RegistryStorage(config, {
|
|
22
|
+
* beforePut: async (ctx) => {
|
|
23
|
+
* const quota = await getQuota(ctx.actor?.orgId);
|
|
24
|
+
* const usage = await getUsage(ctx.actor?.orgId);
|
|
25
|
+
* if (usage + (ctx.metadata?.size || 0) > quota) {
|
|
26
|
+
* return { allowed: false, reason: 'Quota exceeded' };
|
|
27
|
+
* }
|
|
28
|
+
* return { allowed: true };
|
|
29
|
+
* },
|
|
30
|
+
* afterPut: async (ctx) => {
|
|
31
|
+
* await updateUsage(ctx.actor?.orgId, ctx.metadata?.size || 0);
|
|
32
|
+
* }
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
7
35
|
*/
|
|
8
36
|
export class RegistryStorage implements IStorageBackend {
|
|
9
37
|
private smartBucket: plugins.smartbucket.SmartBucket;
|
|
10
38
|
private bucket: plugins.smartbucket.Bucket;
|
|
11
39
|
private bucketName: string;
|
|
40
|
+
private hooks?: IStorageHooks;
|
|
12
41
|
|
|
13
|
-
constructor(private config: IStorageConfig) {
|
|
42
|
+
constructor(private config: IStorageConfig, hooks?: IStorageHooks) {
|
|
14
43
|
this.bucketName = config.bucketName;
|
|
44
|
+
this.hooks = hooks;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Set storage hooks (can be called after construction)
|
|
49
|
+
*/
|
|
50
|
+
public setHooks(hooks: IStorageHooks): void {
|
|
51
|
+
this.hooks = hooks;
|
|
15
52
|
}
|
|
16
53
|
|
|
17
54
|
/**
|
|
@@ -34,7 +71,24 @@ export class RegistryStorage implements IStorageBackend {
|
|
|
34
71
|
*/
|
|
35
72
|
public async getObject(key: string): Promise<Buffer | null> {
|
|
36
73
|
try {
|
|
37
|
-
|
|
74
|
+
const data = await this.bucket.fastGet({ path: key });
|
|
75
|
+
|
|
76
|
+
// Call afterGet hook (non-blocking)
|
|
77
|
+
if (this.hooks?.afterGet && data) {
|
|
78
|
+
const context = this.currentContext;
|
|
79
|
+
if (context) {
|
|
80
|
+
this.hooks.afterGet({
|
|
81
|
+
operation: 'get',
|
|
82
|
+
key,
|
|
83
|
+
protocol: context.protocol,
|
|
84
|
+
actor: context.actor,
|
|
85
|
+
metadata: context.metadata,
|
|
86
|
+
timestamp: new Date(),
|
|
87
|
+
}).catch(() => {}); // Don't fail on hook errors
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return data;
|
|
38
92
|
} catch (error) {
|
|
39
93
|
return null;
|
|
40
94
|
}
|
|
@@ -48,19 +102,159 @@ export class RegistryStorage implements IStorageBackend {
|
|
|
48
102
|
data: Buffer,
|
|
49
103
|
metadata?: Record<string, string>
|
|
50
104
|
): Promise<void> {
|
|
105
|
+
// Call beforePut hook if available
|
|
106
|
+
if (this.hooks?.beforePut) {
|
|
107
|
+
const context = this.currentContext;
|
|
108
|
+
if (context) {
|
|
109
|
+
const hookContext: IStorageHookContext = {
|
|
110
|
+
operation: 'put',
|
|
111
|
+
key,
|
|
112
|
+
protocol: context.protocol,
|
|
113
|
+
actor: context.actor,
|
|
114
|
+
metadata: {
|
|
115
|
+
...context.metadata,
|
|
116
|
+
size: data.length,
|
|
117
|
+
},
|
|
118
|
+
timestamp: new Date(),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const result = await this.hooks.beforePut(hookContext);
|
|
122
|
+
if (!result.allowed) {
|
|
123
|
+
throw new Error(result.reason || 'Storage operation denied by hook');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
51
128
|
// Note: SmartBucket doesn't support metadata yet
|
|
52
129
|
await this.bucket.fastPut({
|
|
53
130
|
path: key,
|
|
54
131
|
contents: data,
|
|
55
132
|
overwrite: true, // Always overwrite existing objects
|
|
56
133
|
});
|
|
134
|
+
|
|
135
|
+
// Call afterPut hook (non-blocking)
|
|
136
|
+
if (this.hooks?.afterPut) {
|
|
137
|
+
const context = this.currentContext;
|
|
138
|
+
if (context) {
|
|
139
|
+
this.hooks.afterPut({
|
|
140
|
+
operation: 'put',
|
|
141
|
+
key,
|
|
142
|
+
protocol: context.protocol,
|
|
143
|
+
actor: context.actor,
|
|
144
|
+
metadata: {
|
|
145
|
+
...context.metadata,
|
|
146
|
+
size: data.length,
|
|
147
|
+
},
|
|
148
|
+
timestamp: new Date(),
|
|
149
|
+
}).catch(() => {}); // Don't fail on hook errors
|
|
150
|
+
}
|
|
151
|
+
}
|
|
57
152
|
}
|
|
58
153
|
|
|
59
154
|
/**
|
|
60
155
|
* Delete an object
|
|
61
156
|
*/
|
|
62
157
|
public async deleteObject(key: string): Promise<void> {
|
|
158
|
+
// Call beforeDelete hook if available
|
|
159
|
+
if (this.hooks?.beforeDelete) {
|
|
160
|
+
const context = this.currentContext;
|
|
161
|
+
if (context) {
|
|
162
|
+
const hookContext: IStorageHookContext = {
|
|
163
|
+
operation: 'delete',
|
|
164
|
+
key,
|
|
165
|
+
protocol: context.protocol,
|
|
166
|
+
actor: context.actor,
|
|
167
|
+
metadata: context.metadata,
|
|
168
|
+
timestamp: new Date(),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const result = await this.hooks.beforeDelete(hookContext);
|
|
172
|
+
if (!result.allowed) {
|
|
173
|
+
throw new Error(result.reason || 'Delete operation denied by hook');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
63
178
|
await this.bucket.fastRemove({ path: key });
|
|
179
|
+
|
|
180
|
+
// Call afterDelete hook (non-blocking)
|
|
181
|
+
if (this.hooks?.afterDelete) {
|
|
182
|
+
const context = this.currentContext;
|
|
183
|
+
if (context) {
|
|
184
|
+
this.hooks.afterDelete({
|
|
185
|
+
operation: 'delete',
|
|
186
|
+
key,
|
|
187
|
+
protocol: context.protocol,
|
|
188
|
+
actor: context.actor,
|
|
189
|
+
metadata: context.metadata,
|
|
190
|
+
timestamp: new Date(),
|
|
191
|
+
}).catch(() => {}); // Don't fail on hook errors
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ========================================================================
|
|
197
|
+
// CONTEXT FOR HOOKS
|
|
198
|
+
// ========================================================================
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Current operation context for hooks.
|
|
202
|
+
* Set this before performing storage operations to enable hooks.
|
|
203
|
+
*/
|
|
204
|
+
private currentContext?: {
|
|
205
|
+
protocol: TRegistryProtocol;
|
|
206
|
+
actor?: IStorageActor;
|
|
207
|
+
metadata?: IStorageMetadata;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Set the current operation context for hooks.
|
|
212
|
+
* Call this before performing storage operations.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```typescript
|
|
216
|
+
* storage.setContext({
|
|
217
|
+
* protocol: 'npm',
|
|
218
|
+
* actor: { userId: 'user123', ip: '192.168.1.1' },
|
|
219
|
+
* metadata: { packageName: 'lodash', version: '4.17.21' }
|
|
220
|
+
* });
|
|
221
|
+
* await storage.putNpmTarball('lodash', '4.17.21', tarball);
|
|
222
|
+
* storage.clearContext();
|
|
223
|
+
* ```
|
|
224
|
+
*/
|
|
225
|
+
public setContext(context: {
|
|
226
|
+
protocol: TRegistryProtocol;
|
|
227
|
+
actor?: IStorageActor;
|
|
228
|
+
metadata?: IStorageMetadata;
|
|
229
|
+
}): void {
|
|
230
|
+
this.currentContext = context;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Clear the current operation context.
|
|
235
|
+
*/
|
|
236
|
+
public clearContext(): void {
|
|
237
|
+
this.currentContext = undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Execute a function with a temporary context.
|
|
242
|
+
* Context is automatically cleared after execution.
|
|
243
|
+
*/
|
|
244
|
+
public async withContext<T>(
|
|
245
|
+
context: {
|
|
246
|
+
protocol: TRegistryProtocol;
|
|
247
|
+
actor?: IStorageActor;
|
|
248
|
+
metadata?: IStorageMetadata;
|
|
249
|
+
},
|
|
250
|
+
fn: () => Promise<T>
|
|
251
|
+
): Promise<T> {
|
|
252
|
+
this.setContext(context);
|
|
253
|
+
try {
|
|
254
|
+
return await fn();
|
|
255
|
+
} finally {
|
|
256
|
+
this.clearContext();
|
|
257
|
+
}
|
|
64
258
|
}
|
|
65
259
|
|
|
66
260
|
/**
|
package/ts/core/index.ts
CHANGED
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
* Core registry infrastructure exports
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// Core interfaces
|
|
6
6
|
export * from './interfaces.core.js';
|
|
7
7
|
|
|
8
|
+
// Auth interfaces and provider
|
|
9
|
+
export * from './interfaces.auth.js';
|
|
10
|
+
export { DefaultAuthProvider } from './classes.defaultauthprovider.js';
|
|
11
|
+
|
|
12
|
+
// Storage interfaces and hooks
|
|
13
|
+
export * from './interfaces.storage.js';
|
|
14
|
+
|
|
8
15
|
// Classes
|
|
9
16
|
export { BaseRegistry } from './classes.baseregistry.js';
|
|
10
17
|
export { RegistryStorage } from './classes.registrystorage.js';
|