@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.
Files changed (110) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/cargo/classes.cargoregistry.d.ts +7 -1
  3. package/dist_ts/cargo/classes.cargoregistry.js +42 -4
  4. package/dist_ts/cargo/classes.cargoupstream.d.ts +44 -0
  5. package/dist_ts/cargo/classes.cargoupstream.js +129 -0
  6. package/dist_ts/cargo/index.d.ts +1 -0
  7. package/dist_ts/cargo/index.js +2 -1
  8. package/dist_ts/classes.smartregistry.d.ts +33 -2
  9. package/dist_ts/classes.smartregistry.js +45 -12
  10. package/dist_ts/composer/classes.composerregistry.d.ts +7 -1
  11. package/dist_ts/composer/classes.composerregistry.js +34 -3
  12. package/dist_ts/composer/classes.composerupstream.d.ts +40 -0
  13. package/dist_ts/composer/classes.composerupstream.js +159 -0
  14. package/dist_ts/composer/index.d.ts +1 -0
  15. package/dist_ts/composer/index.js +2 -1
  16. package/dist_ts/core/classes.authmanager.d.ts +30 -80
  17. package/dist_ts/core/classes.authmanager.js +63 -337
  18. package/dist_ts/core/classes.defaultauthprovider.d.ts +78 -0
  19. package/dist_ts/core/classes.defaultauthprovider.js +311 -0
  20. package/dist_ts/core/classes.registrystorage.d.ts +70 -4
  21. package/dist_ts/core/classes.registrystorage.js +165 -5
  22. package/dist_ts/core/index.d.ts +3 -0
  23. package/dist_ts/core/index.js +7 -2
  24. package/dist_ts/core/interfaces.auth.d.ts +83 -0
  25. package/dist_ts/core/interfaces.auth.js +2 -0
  26. package/dist_ts/core/interfaces.core.d.ts +38 -0
  27. package/dist_ts/core/interfaces.storage.d.ts +120 -0
  28. package/dist_ts/core/interfaces.storage.js +2 -0
  29. package/dist_ts/index.d.ts +1 -0
  30. package/dist_ts/index.js +3 -1
  31. package/dist_ts/maven/classes.mavenregistry.d.ts +12 -1
  32. package/dist_ts/maven/classes.mavenregistry.js +69 -4
  33. package/dist_ts/maven/classes.mavenupstream.d.ts +45 -0
  34. package/dist_ts/maven/classes.mavenupstream.js +153 -0
  35. package/dist_ts/maven/index.d.ts +1 -0
  36. package/dist_ts/maven/index.js +2 -1
  37. package/dist_ts/npm/classes.npmregistry.d.ts +3 -1
  38. package/dist_ts/npm/classes.npmregistry.js +55 -6
  39. package/dist_ts/npm/classes.npmupstream.d.ts +51 -0
  40. package/dist_ts/npm/classes.npmupstream.js +206 -0
  41. package/dist_ts/npm/index.d.ts +1 -0
  42. package/dist_ts/npm/index.js +2 -1
  43. package/dist_ts/oci/classes.ociregistry.d.ts +4 -1
  44. package/dist_ts/oci/classes.ociregistry.js +78 -17
  45. package/dist_ts/oci/classes.ociupstream.d.ts +62 -0
  46. package/dist_ts/oci/classes.ociupstream.js +206 -0
  47. package/dist_ts/oci/index.d.ts +1 -0
  48. package/dist_ts/oci/index.js +2 -1
  49. package/dist_ts/plugins.d.ts +4 -1
  50. package/dist_ts/plugins.js +6 -2
  51. package/dist_ts/pypi/classes.pypiregistry.d.ts +7 -1
  52. package/dist_ts/pypi/classes.pypiregistry.js +60 -4
  53. package/dist_ts/pypi/classes.pypiupstream.d.ts +48 -0
  54. package/dist_ts/pypi/classes.pypiupstream.js +165 -0
  55. package/dist_ts/pypi/index.d.ts +1 -0
  56. package/dist_ts/pypi/index.js +2 -1
  57. package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +7 -1
  58. package/dist_ts/rubygems/classes.rubygemsregistry.js +35 -4
  59. package/dist_ts/rubygems/classes.rubygemsupstream.d.ts +47 -0
  60. package/dist_ts/rubygems/classes.rubygemsupstream.js +184 -0
  61. package/dist_ts/rubygems/index.d.ts +1 -0
  62. package/dist_ts/rubygems/index.js +2 -1
  63. package/dist_ts/upstream/classes.baseupstream.d.ts +112 -0
  64. package/dist_ts/upstream/classes.baseupstream.js +411 -0
  65. package/dist_ts/upstream/classes.circuitbreaker.d.ts +111 -0
  66. package/dist_ts/upstream/classes.circuitbreaker.js +192 -0
  67. package/dist_ts/upstream/classes.upstreamcache.d.ts +170 -0
  68. package/dist_ts/upstream/classes.upstreamcache.js +485 -0
  69. package/dist_ts/upstream/index.d.ts +6 -0
  70. package/dist_ts/upstream/index.js +7 -0
  71. package/dist_ts/upstream/interfaces.upstream.d.ts +169 -0
  72. package/dist_ts/upstream/interfaces.upstream.js +23 -0
  73. package/package.json +4 -2
  74. package/ts/00_commitinfo_data.ts +1 -1
  75. package/ts/cargo/classes.cargoregistry.ts +48 -3
  76. package/ts/cargo/classes.cargoupstream.ts +159 -0
  77. package/ts/cargo/index.ts +1 -0
  78. package/ts/classes.smartregistry.ts +88 -11
  79. package/ts/composer/classes.composerregistry.ts +39 -2
  80. package/ts/composer/classes.composerupstream.ts +200 -0
  81. package/ts/composer/index.ts +1 -0
  82. package/ts/core/classes.authmanager.ts +74 -412
  83. package/ts/core/classes.defaultauthprovider.ts +393 -0
  84. package/ts/core/classes.registrystorage.ts +199 -5
  85. package/ts/core/index.ts +8 -1
  86. package/ts/core/interfaces.auth.ts +91 -0
  87. package/ts/core/interfaces.core.ts +42 -0
  88. package/ts/core/interfaces.storage.ts +130 -0
  89. package/ts/index.ts +3 -0
  90. package/ts/maven/classes.mavenregistry.ts +84 -3
  91. package/ts/maven/classes.mavenupstream.ts +220 -0
  92. package/ts/maven/index.ts +1 -0
  93. package/ts/npm/classes.npmregistry.ts +61 -5
  94. package/ts/npm/classes.npmupstream.ts +260 -0
  95. package/ts/npm/index.ts +1 -0
  96. package/ts/oci/classes.ociregistry.ts +89 -17
  97. package/ts/oci/classes.ociupstream.ts +263 -0
  98. package/ts/oci/index.ts +1 -0
  99. package/ts/plugins.ts +7 -1
  100. package/ts/pypi/classes.pypiregistry.ts +68 -3
  101. package/ts/pypi/classes.pypiupstream.ts +211 -0
  102. package/ts/pypi/index.ts +1 -0
  103. package/ts/rubygems/classes.rubygemsregistry.ts +40 -3
  104. package/ts/rubygems/classes.rubygemsupstream.ts +230 -0
  105. package/ts/rubygems/index.ts +1 -0
  106. package/ts/upstream/classes.baseupstream.ts +526 -0
  107. package/ts/upstream/classes.circuitbreaker.ts +238 -0
  108. package/ts/upstream/classes.upstreamcache.ts +626 -0
  109. package/ts/upstream/index.ts +11 -0
  110. 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
- return await this.bucket.fastGet({ path: key });
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
- // Interfaces
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';