@vltpkg/vsr 0.0.0-27 → 0.0.0-28

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 (80) hide show
  1. package/DEPLOY.md +163 -0
  2. package/LICENSE +114 -10
  3. package/config.ts +221 -0
  4. package/dist/README.md +1 -1
  5. package/dist/bin/vsr.js +8 -6
  6. package/dist/index.js +3 -6
  7. package/dist/index.js.map +2 -2
  8. package/drizzle.config.js +40 -0
  9. package/info/COMPARISONS.md +37 -0
  10. package/info/CONFIGURATION.md +143 -0
  11. package/info/CONTRIBUTING.md +32 -0
  12. package/info/DATABASE_SETUP.md +108 -0
  13. package/info/GRANULAR_ACCESS_TOKENS.md +160 -0
  14. package/info/PROJECT_STRUCTURE.md +291 -0
  15. package/info/ROADMAP.md +27 -0
  16. package/info/SUPPORT.md +39 -0
  17. package/info/TESTING.md +301 -0
  18. package/info/USER_SUPPORT.md +31 -0
  19. package/package.json +49 -6
  20. package/scripts/build-assets.js +31 -0
  21. package/scripts/build-bin.js +63 -0
  22. package/src/assets/public/images/bg.png +0 -0
  23. package/src/assets/public/images/clients/logo-bun.png +0 -0
  24. package/src/assets/public/images/clients/logo-deno.png +0 -0
  25. package/src/assets/public/images/clients/logo-npm.png +0 -0
  26. package/src/assets/public/images/clients/logo-pnpm.png +0 -0
  27. package/src/assets/public/images/clients/logo-vlt.png +0 -0
  28. package/src/assets/public/images/clients/logo-yarn.png +0 -0
  29. package/src/assets/public/images/favicon/apple-touch-icon.png +0 -0
  30. package/src/assets/public/images/favicon/favicon-96x96.png +0 -0
  31. package/src/assets/public/images/favicon/favicon.ico +0 -0
  32. package/src/assets/public/images/favicon/favicon.svg +3 -0
  33. package/src/assets/public/images/favicon/site.webmanifest +21 -0
  34. package/src/assets/public/images/favicon/web-app-manifest-192x192.png +0 -0
  35. package/src/assets/public/images/favicon/web-app-manifest-512x512.png +0 -0
  36. package/src/assets/public/styles/styles.css +231 -0
  37. package/src/bin/demo/package.json +6 -0
  38. package/src/bin/demo/vlt.json +1 -0
  39. package/src/bin/vsr.ts +496 -0
  40. package/src/db/client.ts +590 -0
  41. package/src/db/migrations/0000_faulty_ricochet.sql +14 -0
  42. package/src/db/migrations/0000_initial.sql +29 -0
  43. package/src/db/migrations/0001_uuid_validation.sql +35 -0
  44. package/src/db/migrations/0001_wealthy_magdalene.sql +7 -0
  45. package/src/db/migrations/drop.sql +3 -0
  46. package/src/db/migrations/meta/0000_snapshot.json +104 -0
  47. package/src/db/migrations/meta/0001_snapshot.json +155 -0
  48. package/src/db/migrations/meta/_journal.json +20 -0
  49. package/src/db/schema.ts +43 -0
  50. package/src/index.ts +434 -0
  51. package/src/middleware/config.ts +79 -0
  52. package/src/middleware/telemetry.ts +43 -0
  53. package/src/queue/index.ts +97 -0
  54. package/src/routes/access.ts +852 -0
  55. package/src/routes/docs.ts +63 -0
  56. package/src/routes/misc.ts +469 -0
  57. package/src/routes/packages.ts +2823 -0
  58. package/src/routes/ping.ts +39 -0
  59. package/src/routes/search.ts +131 -0
  60. package/src/routes/static.ts +74 -0
  61. package/src/routes/tokens.ts +259 -0
  62. package/src/routes/users.ts +68 -0
  63. package/src/utils/auth.ts +202 -0
  64. package/src/utils/cache.ts +587 -0
  65. package/src/utils/config.ts +50 -0
  66. package/src/utils/database.ts +69 -0
  67. package/src/utils/docs.ts +146 -0
  68. package/src/utils/packages.ts +453 -0
  69. package/src/utils/response.ts +125 -0
  70. package/src/utils/routes.ts +64 -0
  71. package/src/utils/spa.ts +52 -0
  72. package/src/utils/tracing.ts +52 -0
  73. package/src/utils/upstream.ts +172 -0
  74. package/tsconfig.json +16 -0
  75. package/tsconfig.worker.json +3 -0
  76. package/typedoc.mjs +2 -0
  77. package/types.ts +598 -0
  78. package/vitest.config.ts +25 -0
  79. package/vlt.json.example +56 -0
  80. package/wrangler.json +65 -0
package/types.ts ADDED
@@ -0,0 +1,598 @@
1
+ // ---------------------------------------------------------
2
+ // Consolidated TypeScript Types
3
+ // ---------------------------------------------------------
4
+
5
+ import type { Context } from 'hono'
6
+ import type { D1Database } from '@cloudflare/workers-types'
7
+
8
+ // ---------------------------------------------------------
9
+ // Request Types
10
+ // ---------------------------------------------------------
11
+
12
+ export interface _LoginRequestBody {
13
+ name?: string
14
+ password?: string
15
+ email?: string
16
+ [key: string]: unknown
17
+ }
18
+
19
+ export interface _AuditRequestBody {
20
+ requires?: Record<string, string>
21
+ [key: string]: unknown
22
+ }
23
+
24
+ export interface _UpstreamPackageData {
25
+ 'dist-tags'?: Record<string, string>
26
+ versions?: Record<string, unknown>
27
+ time?: Record<string, string>
28
+ [key: string]: unknown
29
+ }
30
+
31
+ export interface _DashboardData {
32
+ [key: string]: unknown
33
+ }
34
+
35
+ export interface RequestQueueMessage {
36
+ body: {
37
+ type: 'package_refresh' | 'version_refresh'
38
+ packageName?: string
39
+ spec?: string
40
+ upstream: string
41
+ options: Record<string, unknown>
42
+ }
43
+ ack(): void
44
+ retry(): void
45
+ }
46
+
47
+ export interface QueueBatch {
48
+ messages: RequestQueueMessage[]
49
+ }
50
+
51
+ // ---------------------------------------------------------
52
+ // Database Types
53
+ // ---------------------------------------------------------
54
+
55
+ export interface Package {
56
+ name: string
57
+ tags: string // JSON string containing Record<string, string>
58
+ lastUpdated?: string
59
+ origin: 'local' | 'upstream'
60
+ upstream?: string
61
+ cachedAt?: string
62
+ }
63
+
64
+ export interface ParsedPackage {
65
+ name: string
66
+ tags: Record<string, string>
67
+ lastUpdated: string | null
68
+ origin: string | null
69
+ upstream: string | null
70
+ cachedAt: string | null
71
+ }
72
+
73
+ export interface Version {
74
+ spec: string
75
+ manifest: string // JSON string containing PackageManifest
76
+ publishedAt?: string
77
+ origin: 'local' | 'upstream'
78
+ upstream?: string
79
+ cachedAt?: string
80
+ }
81
+
82
+ export interface ParsedVersion {
83
+ spec: string
84
+ version: string
85
+ manifest: Record<string, any>
86
+ published_at: string | null
87
+ origin: string | null
88
+ upstream: string | null
89
+ cachedAt: string | null
90
+ }
91
+
92
+ export interface Token {
93
+ token: string
94
+ uuid: string
95
+ scope: string // JSON string containing TokenScope[]
96
+ }
97
+
98
+ export interface ParsedToken {
99
+ token: string
100
+ uuid: string
101
+ scope: TokenScope[]
102
+ }
103
+
104
+ // ---------------------------------------------------------
105
+ // Authentication & Authorization Types
106
+ // ---------------------------------------------------------
107
+
108
+ export interface TokenScope {
109
+ values: string[]
110
+ types: {
111
+ pkg?: { read: boolean; write: boolean }
112
+ user?: { read: boolean; write: boolean }
113
+ }
114
+ }
115
+
116
+ export interface TokenAccess {
117
+ anyUser: boolean
118
+ specificUser: boolean
119
+ anyPackage: boolean
120
+ specificPackage: boolean
121
+ readAccess: boolean
122
+ writeAccess: boolean
123
+ methods: string[]
124
+ }
125
+
126
+ export interface AuthUser {
127
+ uuid: string | null
128
+ scope: TokenScope[] | null
129
+ token: string
130
+ }
131
+
132
+ // ---------------------------------------------------------
133
+ // Package & Manifest Types
134
+ // ---------------------------------------------------------
135
+
136
+ export interface PackageManifest {
137
+ name: string
138
+ version: string
139
+ description?: string
140
+ main?: string
141
+ module?: string
142
+ types?: string
143
+ bin?: Record<string, string> | string
144
+ scripts?: Record<string, string>
145
+ dependencies?: Record<string, string>
146
+ devDependencies?: Record<string, string>
147
+ peerDependencies?: Record<string, string>
148
+ optionalDependencies?: Record<string, string>
149
+ peerDependenciesMeta?: Record<string, { optional?: boolean }>
150
+ engines?: Record<string, string>
151
+ os?: string[]
152
+ cpu?: string[]
153
+ keywords?: string[]
154
+ author?: string | { name: string; email?: string; url?: string }
155
+ contributors?: (
156
+ | string
157
+ | { name: string; email?: string; url?: string }
158
+ )[]
159
+ license?: string
160
+ repository?:
161
+ | string
162
+ | { type: string; url: string; directory?: string }
163
+ bugs?: string | { url: string; email?: string }
164
+ homepage?: string
165
+ files?: string[]
166
+ publishConfig?: Record<string, any>
167
+ dist?: {
168
+ tarball: string
169
+ shasum?: string
170
+ integrity?: string
171
+ fileCount?: number
172
+ unpackedSize?: number
173
+ }
174
+ _id?: string
175
+ _rev?: string
176
+ _attachments?: Record<string, any>
177
+ [key: string]: any // Allow additional properties
178
+ }
179
+
180
+ export interface SlimmedManifest {
181
+ name: string
182
+ version: string
183
+ dependencies?: Record<string, string>
184
+ peerDependencies?: Record<string, string>
185
+ optionalDependencies?: Record<string, string>
186
+ peerDependenciesMeta?: Record<string, { optional?: boolean }>
187
+ bin?: Record<string, string> | string
188
+ engines?: Record<string, string>
189
+ dist: {
190
+ tarball: string
191
+ }
192
+ }
193
+
194
+ export interface Packument {
195
+ name: string
196
+ 'dist-tags': Record<string, string>
197
+ versions: Record<string, SlimmedManifest>
198
+ time: Record<string, string> & {
199
+ modified: string
200
+ }
201
+ }
202
+
203
+ export interface PackageSpec {
204
+ name?: string
205
+ pkg?: string
206
+ scope?: string
207
+ }
208
+
209
+ // ---------------------------------------------------------
210
+ // Upstream & Configuration Types
211
+ // ---------------------------------------------------------
212
+
213
+ export interface UpstreamConfig {
214
+ type: 'local' | 'npm' | 'vsr' | 'jsr'
215
+ url: string
216
+ allowPublish?: boolean
217
+ }
218
+
219
+ export interface OriginConfig {
220
+ default: string
221
+ upstreams: Record<string, UpstreamConfig>
222
+ }
223
+
224
+ export interface ParsedPackageInfo {
225
+ upstream?: string
226
+ packageName: string
227
+ version?: string
228
+ segments: string[]
229
+ }
230
+
231
+ // ---------------------------------------------------------
232
+ // Cache Types
233
+ // ---------------------------------------------------------
234
+
235
+ export interface CacheOptions {
236
+ packumentTtlMinutes?: number
237
+ manifestTtlMinutes?: number
238
+ staleWhileRevalidateMinutes?: number
239
+ forceRefresh?: boolean
240
+ upstream?: string
241
+ }
242
+
243
+ export interface CacheResult<T> {
244
+ data?: T
245
+ package?: T // For package cache results
246
+ version?: T // For version cache results
247
+ fromCache: boolean
248
+ stale?: boolean
249
+ }
250
+
251
+ export interface CacheValidation {
252
+ valid: boolean
253
+ stale: boolean
254
+ data: any
255
+ }
256
+
257
+ export interface QueueMessage {
258
+ type: 'package_refresh' | 'version_refresh'
259
+ packageName?: string
260
+ spec?: string
261
+ upstream: string
262
+ timestamp: number
263
+ options: {
264
+ packumentTtlMinutes?: number
265
+ manifestTtlMinutes?: number
266
+ upstream?: string
267
+ }
268
+ }
269
+
270
+ // ---------------------------------------------------------
271
+ // Request Context Types
272
+ // ---------------------------------------------------------
273
+
274
+ export interface RequestContext {
275
+ protocol?: string
276
+ host?: string
277
+ upstream?: string
278
+ }
279
+
280
+ export interface DatabaseOperations {
281
+ // Package operations
282
+ getPackage(name: string): Promise<ParsedPackage | null>
283
+ upsertPackage(
284
+ name: string,
285
+ tags: Record<string, string>,
286
+ lastUpdated?: string,
287
+ ): Promise<any>
288
+ upsertCachedPackage(
289
+ name: string,
290
+ tags: Record<string, string>,
291
+ upstream: string,
292
+ lastUpdated?: string,
293
+ ): Promise<any>
294
+ getCachedPackage(name: string): Promise<ParsedPackage | null>
295
+ isPackageCacheValid(
296
+ name: string,
297
+ ttlMinutes?: number,
298
+ ): Promise<boolean>
299
+
300
+ // Token operations
301
+ getToken(token: string): Promise<ParsedToken | null>
302
+ validateTokenAccess(
303
+ authToken: string,
304
+ targetUuid: string,
305
+ ): Promise<boolean>
306
+ upsertToken(
307
+ token: string,
308
+ uuid: string,
309
+ scope: TokenScope[],
310
+ authToken?: string,
311
+ ): Promise<any>
312
+ deleteToken(token: string, authToken?: string): Promise<any>
313
+
314
+ // Version operations
315
+ getVersion(spec: string): Promise<ParsedVersion | null>
316
+ upsertVersion(
317
+ spec: string,
318
+ manifest: PackageManifest,
319
+ publishedAt: string,
320
+ ): Promise<any>
321
+ upsertCachedVersion(
322
+ spec: string,
323
+ manifest: PackageManifest,
324
+ upstream: string,
325
+ publishedAt: string,
326
+ ): Promise<any>
327
+ getCachedVersion(spec: string): Promise<ParsedVersion | null>
328
+ isVersionCacheValid(
329
+ spec: string,
330
+ ttlMinutes?: number,
331
+ ): Promise<boolean>
332
+
333
+ // Search operations
334
+ searchPackages(
335
+ query: string,
336
+ scope?: string,
337
+ ): Promise<SearchResult[]>
338
+ getVersionsByPackage(packageName: string): Promise<ParsedVersion[]>
339
+ }
340
+
341
+ export type HonoContext = Context<{
342
+ Bindings: Environment
343
+ Variables: {
344
+ db: DatabaseOperations
345
+ upstream?: string
346
+ }
347
+ }> & {
348
+ waitUntil?: (promise: Promise<any>) => void
349
+ executionCtx?: {
350
+ waitUntil: (promise: Promise<any>) => void
351
+ }
352
+ }
353
+
354
+ // ---------------------------------------------------------
355
+ // API Response Types
356
+ // ---------------------------------------------------------
357
+
358
+ export interface ApiError {
359
+ error: string
360
+ message?: string
361
+ details?: any
362
+ }
363
+
364
+ export interface TokenCreateRequest {
365
+ uuid?: string
366
+ scope: TokenScope[]
367
+ }
368
+
369
+ export interface TokenCreateResponse {
370
+ token: string
371
+ uuid: string
372
+ scope: TokenScope[]
373
+ }
374
+
375
+ export interface AccessRequest {
376
+ username: string
377
+ permission: 'read-only' | 'read-write'
378
+ }
379
+
380
+ export interface AccessResponse {
381
+ name: string
382
+ collaborators: Record<string, 'read-only' | 'read-write'>
383
+ }
384
+
385
+ // ---------------------------------------------------------
386
+ // Utility Types
387
+ // ---------------------------------------------------------
388
+
389
+ export type HttpMethod =
390
+ | 'GET'
391
+ | 'POST'
392
+ | 'PUT'
393
+ | 'DELETE'
394
+ | 'PATCH'
395
+ | 'HEAD'
396
+ | 'OPTIONS'
397
+
398
+ export interface ValidationResult {
399
+ valid: boolean
400
+ errors?: string[]
401
+ }
402
+
403
+ export interface FileInfo {
404
+ path: string
405
+ content: Uint8Array | string
406
+ size: number
407
+ }
408
+
409
+ // ---------------------------------------------------------
410
+ // Constants Types
411
+ // ---------------------------------------------------------
412
+
413
+ export interface CookieOptions {
414
+ path: string
415
+ httpOnly: boolean
416
+ secure: boolean
417
+ sameSite: 'strict' | 'lax' | 'none'
418
+ }
419
+
420
+ export interface ApiDocsConfig {
421
+ metaData: {
422
+ title: string
423
+ }
424
+ hideModels: boolean
425
+ hideDownloadButton: boolean
426
+ darkMode: boolean
427
+ favicon: string
428
+ defaultHttpClient: {
429
+ targetKey: string
430
+ clientKey: string
431
+ }
432
+ authentication: {
433
+ http: {
434
+ bearer: { token: string }
435
+ basic: { username: string; password: string }
436
+ }
437
+ }
438
+ hiddenClients: Record<string, boolean | string[]>
439
+ spec: {
440
+ content: any
441
+ }
442
+ customCss: string
443
+ }
444
+
445
+ // ---------------------------------------------------------
446
+ // Authentication Provider Types (WorkOS)
447
+ // ---------------------------------------------------------
448
+
449
+ export interface WorkOSUser {
450
+ id: string
451
+ email: string
452
+ firstName?: string
453
+ lastName?: string
454
+ profilePictureUrl?: string
455
+ createdAt: string
456
+ updatedAt: string
457
+ }
458
+
459
+ export interface WorkOSAuthResponse {
460
+ authenticated: boolean
461
+ sessionId?: string
462
+ organizationId?: string
463
+ role?: string
464
+ permissions?: string[]
465
+ user?: WorkOSUser
466
+ reason?: string
467
+ }
468
+
469
+ export interface WorkOSAuthResult {
470
+ user: WorkOSUser
471
+ sealedSession: string
472
+ }
473
+
474
+ // ---------------------------------------------------------
475
+ // Search Types
476
+ // ---------------------------------------------------------
477
+
478
+ export interface SearchResult {
479
+ name: string
480
+ tags: Record<string, string>
481
+ version?: string
482
+ description?: string
483
+ keywords?: string[]
484
+ lastUpdated?: string
485
+ homepage?: string
486
+ repository?: string
487
+ bugs?: string
488
+ author?: string
489
+ publisher?: string
490
+ maintainers?: string[]
491
+ }
492
+
493
+ export interface SearchResponse {
494
+ objects: {
495
+ package: {
496
+ name: string
497
+ scope: string
498
+ version: string
499
+ description: string
500
+ keywords: string[]
501
+ date: string
502
+ links: {
503
+ npm: string
504
+ homepage?: string
505
+ repository?: string
506
+ bugs?: string
507
+ }
508
+ author?: string
509
+ publisher?: string
510
+ maintainers: string[]
511
+ }
512
+ score: {
513
+ final: number
514
+ detail: {
515
+ quality: number
516
+ popularity: number
517
+ maintenance: number
518
+ }
519
+ }
520
+ searchScore: number
521
+ }[]
522
+ total: number
523
+ time: string
524
+ }
525
+
526
+ // ---------------------------------------------------------
527
+ // Environment Types
528
+ // ---------------------------------------------------------
529
+
530
+ export interface Environment {
531
+ // Database
532
+ D1_DATABASE?: D1Database
533
+ DB?: D1Database
534
+
535
+ // Queue and Storage
536
+ CACHE_REFRESH_QUEUE?: any
537
+ BUCKET?: any
538
+ ASSETS?: any
539
+
540
+ // Authentication
541
+ WORKOS_API_KEY?: string
542
+ WORKOS_CLIENT_ID?: string
543
+ WORKOS_PROVIDER?: string
544
+ WORKOS_REDIRECT_URI?: string
545
+ WORKOS_COOKIE_PASSWORD?: string
546
+
547
+ // Configuration flags (enriched by configMiddleware)
548
+ DAEMON_ENABLED?: boolean
549
+ TELEMETRY_ENABLED?: boolean
550
+ API_DOCS_ENABLED?: boolean
551
+ DEBUG_ENABLED?: boolean
552
+
553
+ // Telemetry
554
+ SENTRY?: {
555
+ dsn: string
556
+ environment?: string
557
+ }
558
+ SENTRY_CONFIG?: {
559
+ dsn: string
560
+ environment?: string
561
+ sendDefaultPii?: boolean
562
+ sampleRate?: number
563
+ tracesSampleRate?: number
564
+ }
565
+
566
+ // Cloudflare specific
567
+ CF?: {
568
+ connecting_ip?: string
569
+ }
570
+
571
+ // Other config values
572
+ DAEMON_PORT?: number
573
+ DAEMON_URL?: string
574
+ PORT?: number
575
+ VERSION?: string
576
+ URL?: string
577
+
578
+ [key: string]: any
579
+ }
580
+
581
+ export interface Args {
582
+ daemon: boolean
583
+ telemetry: boolean
584
+ debug: boolean
585
+ help: boolean
586
+ port: number
587
+ config?: string
588
+ env?: string
589
+ 'db-name'?: string
590
+ 'bucket-name'?: string
591
+ 'queue-name'?: string
592
+ 'dry-run'?: boolean
593
+ }
594
+
595
+ export interface MinArgsResult {
596
+ args: Args
597
+ positionals: string[]
598
+ }
@@ -0,0 +1,25 @@
1
+ import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'
2
+
3
+ export default defineWorkersConfig({
4
+ test: {
5
+ globals: true,
6
+ testTimeout: 30000, // 30 seconds timeout for upstream requests
7
+ hookTimeout: 10000, // 10 seconds for setup/teardown
8
+ setupFiles: ['./test/setup.ts'],
9
+ poolOptions: {
10
+ workers: {
11
+ wrangler: {
12
+ configPath: './wrangler.json',
13
+ },
14
+ miniflare: {
15
+ bindings: {
16
+ REAL_PLATFORM: process.platform,
17
+ },
18
+ },
19
+ },
20
+ },
21
+ },
22
+ esbuild: {
23
+ target: 'es2022',
24
+ },
25
+ })
@@ -0,0 +1,56 @@
1
+ {
2
+ "registry": {
3
+ "daemon": true,
4
+ "telemetry": true,
5
+ "debug": false,
6
+ "port": 1337,
7
+ "deploy": {
8
+ "sentry": {
9
+ "dsn": "https://your-sentry-dsn@sentry.io/project-id",
10
+ "sampleRate": 1.0,
11
+ "tracesSampleRate": 0.1
12
+ },
13
+ "environments": {
14
+ "dev": {
15
+ "databaseName": "vsr-dev-database",
16
+ "bucketName": "vsr-dev-bucket",
17
+ "queueName": "vsr-dev-cache-refresh-queue",
18
+ "sentry": {
19
+ "environment": "development"
20
+ },
21
+ "vars": {
22
+ "CUSTOM_VAR": "dev-value",
23
+ "API_BASE_URL": "https://dev-api.example.com"
24
+ }
25
+ },
26
+ "staging": {
27
+ "databaseName": "vsr-staging-database",
28
+ "bucketName": "vsr-staging-bucket",
29
+ "queueName": "vsr-staging-cache-refresh-queue",
30
+ "sentry": {
31
+ "environment": "staging"
32
+ },
33
+ "vars": {
34
+ "CUSTOM_VAR": "staging-value",
35
+ "API_BASE_URL": "https://staging-api.example.com"
36
+ }
37
+ },
38
+ "prod": {
39
+ "databaseName": "vsr-prod-database",
40
+ "bucketName": "vsr-prod-bucket",
41
+ "queueName": "vsr-prod-cache-refresh-queue",
42
+ "sentry": {
43
+ "environment": "production",
44
+ "dsn": "https://your-prod-sentry-dsn@sentry.io/project-id",
45
+ "sampleRate": 0.1,
46
+ "tracesSampleRate": 0.01
47
+ },
48
+ "vars": {
49
+ "CUSTOM_VAR": "prod-value",
50
+ "API_BASE_URL": "https://api.example.com"
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }