digital-products 2.1.3 → 2.3.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 (122) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +2 -0
  3. package/dist/api.js +7 -7
  4. package/dist/api.js.map +1 -1
  5. package/dist/app.js +6 -6
  6. package/dist/app.js.map +1 -1
  7. package/dist/client.d.ts +157 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +69 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/content.js +7 -7
  12. package/dist/content.js.map +1 -1
  13. package/dist/data.d.ts.map +1 -1
  14. package/dist/data.js +6 -6
  15. package/dist/data.js.map +1 -1
  16. package/dist/dataset.js +5 -5
  17. package/dist/dataset.js.map +1 -1
  18. package/dist/index.d.ts +92 -13
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +139 -15
  21. package/dist/index.js.map +1 -1
  22. package/dist/mcp.d.ts +1 -1
  23. package/dist/mcp.d.ts.map +1 -1
  24. package/dist/mcp.js +17 -10
  25. package/dist/mcp.js.map +1 -1
  26. package/dist/product.js +2 -2
  27. package/dist/product.js.map +1 -1
  28. package/dist/sdk.d.ts.map +1 -1
  29. package/dist/sdk.js +52 -16
  30. package/dist/sdk.js.map +1 -1
  31. package/dist/site.d.ts.map +1 -1
  32. package/dist/site.js +12 -8
  33. package/dist/site.js.map +1 -1
  34. package/dist/types.d.ts +830 -12
  35. package/dist/types.d.ts.map +1 -1
  36. package/dist/types.js +495 -2
  37. package/dist/types.js.map +1 -1
  38. package/dist/worker.d.ts +205 -0
  39. package/dist/worker.d.ts.map +1 -0
  40. package/dist/worker.js +356 -0
  41. package/dist/worker.js.map +1 -0
  42. package/package.json +29 -13
  43. package/src/api.ts +7 -7
  44. package/src/app.ts +6 -6
  45. package/src/client.ts +192 -0
  46. package/src/content.ts +7 -7
  47. package/src/data.ts +12 -7
  48. package/src/dataset.ts +5 -5
  49. package/src/index.ts +151 -15
  50. package/src/mcp.ts +18 -11
  51. package/src/product.ts +2 -2
  52. package/src/sdk.ts +54 -15
  53. package/src/site.ts +12 -8
  54. package/src/types.ts +821 -12
  55. package/src/worker.ts +525 -0
  56. package/test/product.test.ts +53 -198
  57. package/test/unified-types.test.ts +589 -0
  58. package/test/worker.test.ts +912 -0
  59. package/vitest.config.ts +42 -0
  60. package/wrangler.jsonc +36 -0
  61. package/.turbo/turbo-build.log +0 -5
  62. package/LICENSE +0 -21
  63. package/dist/features/define.d.ts +0 -63
  64. package/dist/features/define.d.ts.map +0 -1
  65. package/dist/features/define.js +0 -72
  66. package/dist/features/define.js.map +0 -1
  67. package/dist/features/flags.d.ts +0 -98
  68. package/dist/features/flags.d.ts.map +0 -1
  69. package/dist/features/flags.js +0 -145
  70. package/dist/features/flags.js.map +0 -1
  71. package/dist/features/toggles.d.ts +0 -75
  72. package/dist/features/toggles.d.ts.map +0 -1
  73. package/dist/features/toggles.js +0 -107
  74. package/dist/features/toggles.js.map +0 -1
  75. package/dist/tiers/define.d.ts +0 -63
  76. package/dist/tiers/define.d.ts.map +0 -1
  77. package/dist/tiers/define.js +0 -78
  78. package/dist/tiers/define.js.map +0 -1
  79. package/dist/tiers/entitlements.d.ts +0 -94
  80. package/dist/tiers/entitlements.d.ts.map +0 -1
  81. package/dist/tiers/entitlements.js +0 -94
  82. package/dist/tiers/entitlements.js.map +0 -1
  83. package/src/api.js +0 -128
  84. package/src/app.js +0 -106
  85. package/src/content.js +0 -77
  86. package/src/data.js +0 -106
  87. package/src/dataset.js +0 -49
  88. package/src/entities/ai.js +0 -858
  89. package/src/entities/content.js +0 -783
  90. package/src/entities/index.js +0 -88
  91. package/src/entities/interfaces.js +0 -929
  92. package/src/entities/lifecycle.js +0 -803
  93. package/src/entities/products.js +0 -797
  94. package/src/entities/web.js +0 -657
  95. package/src/features/define.ts +0 -130
  96. package/src/features/flags.ts +0 -247
  97. package/src/features/toggles.ts +0 -189
  98. package/src/index.js +0 -35
  99. package/src/mcp.js +0 -139
  100. package/src/pricing/billing.ts +0 -386
  101. package/src/pricing/plans.ts +0 -214
  102. package/src/product.js +0 -53
  103. package/src/registry.js +0 -31
  104. package/src/sdk.js +0 -127
  105. package/src/site.js +0 -112
  106. package/src/tiers/define.ts +0 -137
  107. package/src/tiers/entitlements.ts +0 -201
  108. package/src/types.js +0 -4
  109. package/test/analytics/events.test.ts +0 -319
  110. package/test/analytics/experiments.test.ts +0 -327
  111. package/test/features/define.test.ts +0 -187
  112. package/test/features/flags.test.ts +0 -259
  113. package/test/features/toggles.test.ts +0 -178
  114. package/test/lifecycle/stages.test.ts +0 -233
  115. package/test/lifecycle/transitions.test.ts +0 -207
  116. package/test/onboarding/flows.test.ts +0 -307
  117. package/test/pricing/billing.test.ts +0 -287
  118. package/test/pricing/plans.test.ts +0 -307
  119. package/test/roadmap/milestones.test.ts +0 -231
  120. package/test/roadmap/priorities.test.ts +0 -239
  121. package/test/tiers/define.test.ts +0 -192
  122. package/test/tiers/entitlements.test.ts +0 -220
package/src/worker.ts ADDED
@@ -0,0 +1,525 @@
1
+ /**
2
+ * Worker Export - WorkerEntrypoint for RPC access to Digital Products
3
+ *
4
+ * Exposes ProductServiceCore methods via Cloudflare RPC.
5
+ * Uses in-memory storage for products, features, versions, and bundles.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // wrangler.jsonc
10
+ * {
11
+ * "services": [
12
+ * { "binding": "PRODUCTS", "service": "digital-products" }
13
+ * ]
14
+ * }
15
+ *
16
+ * // worker.ts - consuming service
17
+ * export default {
18
+ * async fetch(request: Request, env: Env) {
19
+ * const service = env.PRODUCTS.connect()
20
+ * const product = await service.create({ name: 'My Product', description: 'Test', version: '1.0.0' })
21
+ * return Response.json(product)
22
+ * }
23
+ * }
24
+ * ```
25
+ *
26
+ * @packageDocumentation
27
+ */
28
+
29
+ // @ts-expect-error cloudflare:workers is a Cloudflare Workers runtime module
30
+ import { WorkerEntrypoint, RpcTarget } from 'cloudflare:workers'
31
+
32
+ // =============================================================================
33
+ // Types
34
+ // =============================================================================
35
+
36
+ export interface ProductData {
37
+ id: string
38
+ name: string
39
+ description: string
40
+ version: string
41
+ status: 'draft' | 'active' | 'deprecated' | 'archived'
42
+ type?: string
43
+ features?: FeatureData[]
44
+ metadata?: Record<string, unknown>
45
+ tags?: string[]
46
+ createdAt: Date
47
+ updatedAt: Date
48
+ }
49
+
50
+ export interface FeatureData {
51
+ id: string
52
+ name: string
53
+ description: string
54
+ status: 'draft' | 'beta' | 'ga' | 'deprecated'
55
+ productId: string
56
+ metadata?: Record<string, unknown>
57
+ createdAt: Date
58
+ updatedAt: Date
59
+ }
60
+
61
+ export interface VersionData {
62
+ id: string
63
+ productId: string
64
+ version: string
65
+ changelog?: string
66
+ status: 'draft' | 'published' | 'deprecated'
67
+ features?: string[]
68
+ metadata?: Record<string, unknown>
69
+ createdAt: Date
70
+ publishedAt?: Date
71
+ }
72
+
73
+ export interface BundleData {
74
+ id: string
75
+ name: string
76
+ description: string
77
+ productIds: string[]
78
+ pricing?: PricingData
79
+ metadata?: Record<string, unknown>
80
+ createdAt: Date
81
+ updatedAt: Date
82
+ }
83
+
84
+ export interface PricingData {
85
+ model: 'free' | 'one-time' | 'subscription' | 'usage-based'
86
+ amount?: number
87
+ currency?: string
88
+ interval?: 'month' | 'year'
89
+ }
90
+
91
+ export interface ListOptions {
92
+ limit?: number
93
+ offset?: number
94
+ status?: string
95
+ type?: string
96
+ orderBy?: string
97
+ order?: 'asc' | 'desc'
98
+ }
99
+
100
+ /**
101
+ * DurableObjectNamespace type declaration for Cloudflare Workers
102
+ */
103
+ declare interface DurableObjectNamespace {
104
+ idFromName(name: string): DurableObjectId
105
+ idFromString(id: string): DurableObjectId
106
+ newUniqueId(): DurableObjectId
107
+ get(id: DurableObjectId): DurableObjectStub
108
+ }
109
+
110
+ declare interface DurableObjectId {
111
+ toString(): string
112
+ }
113
+
114
+ declare interface DurableObjectStub {
115
+ fetch(request: Request): Promise<Response>
116
+ }
117
+
118
+ /**
119
+ * Environment bindings for the worker
120
+ */
121
+ export interface Env {
122
+ AI?: unknown
123
+ PRODUCT_CATALOG?: DurableObjectNamespace
124
+ }
125
+
126
+ // =============================================================================
127
+ // In-Memory Storage
128
+ // =============================================================================
129
+
130
+ // Global storage maps for persistence across connect() calls
131
+ const products = new Map<string, ProductData>()
132
+ const features = new Map<string, FeatureData>()
133
+ const versions = new Map<string, VersionData>()
134
+ const bundles = new Map<string, BundleData>()
135
+
136
+ /**
137
+ * Generate a unique ID
138
+ */
139
+ function generateId(): string {
140
+ return crypto.randomUUID()
141
+ }
142
+
143
+ // =============================================================================
144
+ // ProductServiceCore
145
+ // =============================================================================
146
+
147
+ /**
148
+ * ProductServiceCore - RpcTarget with all product operations
149
+ *
150
+ * Provides CRUD for products, features, versions, and bundles.
151
+ */
152
+ export class ProductServiceCore extends RpcTarget {
153
+ private env: Env
154
+
155
+ constructor(env: Env) {
156
+ super()
157
+ this.env = env
158
+ }
159
+
160
+ // ==================== Product CRUD ====================
161
+
162
+ /**
163
+ * Create a new product
164
+ */
165
+ async create(data: Partial<ProductData>): Promise<ProductData> {
166
+ // Validate required fields
167
+ if (!data.name || !data.description || !data.version) {
168
+ throw new Error('Missing required fields: name, description, and version are required')
169
+ }
170
+
171
+ const now = new Date()
172
+ const product: ProductData = {
173
+ id: data.id || generateId(),
174
+ name: data.name,
175
+ description: data.description,
176
+ version: data.version,
177
+ status: data.status || 'draft',
178
+ createdAt: now,
179
+ updatedAt: now,
180
+ ...(data.type !== undefined && { type: data.type }),
181
+ ...(data.metadata !== undefined && { metadata: data.metadata }),
182
+ ...(data.tags !== undefined && { tags: data.tags }),
183
+ }
184
+
185
+ products.set(product.id, product)
186
+ return product
187
+ }
188
+
189
+ /**
190
+ * Get a product by ID
191
+ */
192
+ async get(id: string): Promise<ProductData | null> {
193
+ return products.get(id) || null
194
+ }
195
+
196
+ /**
197
+ * Update a product
198
+ */
199
+ async update(id: string, data: Partial<ProductData>): Promise<ProductData> {
200
+ const existing = products.get(id)
201
+ if (!existing) {
202
+ throw new Error(`Product not found: ${id}`)
203
+ }
204
+
205
+ const updated: ProductData = {
206
+ ...existing,
207
+ ...(data as unknown as Record<string, unknown>),
208
+ id: existing.id, // ID cannot be changed
209
+ createdAt: existing.createdAt, // createdAt cannot be changed
210
+ updatedAt: new Date(),
211
+ // Merge metadata if provided
212
+ ...(data.metadata ? { metadata: { ...existing.metadata, ...data.metadata } } : {}),
213
+ } as unknown as ProductData
214
+
215
+ products.set(id, updated)
216
+ return updated
217
+ }
218
+
219
+ /**
220
+ * Delete a product
221
+ */
222
+ async delete(id: string): Promise<boolean> {
223
+ const exists = products.has(id)
224
+ if (!exists) {
225
+ return false
226
+ }
227
+
228
+ // Cascade delete features
229
+ for (const [featureId, feature] of features) {
230
+ if (feature.productId === id) {
231
+ features.delete(featureId)
232
+ }
233
+ }
234
+
235
+ // Cascade delete versions
236
+ for (const [versionId, version] of versions) {
237
+ if (version.productId === id) {
238
+ versions.delete(versionId)
239
+ }
240
+ }
241
+
242
+ products.delete(id)
243
+ return true
244
+ }
245
+
246
+ /**
247
+ * List products with optional filtering
248
+ */
249
+ async list(options?: ListOptions): Promise<ProductData[]> {
250
+ let result = Array.from(products.values())
251
+
252
+ // Filter by status
253
+ if (options?.status) {
254
+ result = result.filter((p) => p.status === options.status)
255
+ }
256
+
257
+ // Filter by type
258
+ if (options?.type) {
259
+ result = result.filter((p) => p.type === options.type)
260
+ }
261
+
262
+ // Sort
263
+ if (options?.orderBy) {
264
+ const order = options.order || 'asc'
265
+ result.sort((a, b) => {
266
+ const aVal = (a as unknown as Record<string, unknown>)[options.orderBy!]
267
+ const bVal = (b as unknown as Record<string, unknown>)[options.orderBy!]
268
+
269
+ if (typeof aVal === 'string' && typeof bVal === 'string') {
270
+ return order === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal)
271
+ }
272
+
273
+ if (aVal instanceof Date && bVal instanceof Date) {
274
+ return order === 'asc' ? aVal.getTime() - bVal.getTime() : bVal.getTime() - aVal.getTime()
275
+ }
276
+
277
+ return 0
278
+ })
279
+ }
280
+
281
+ // Pagination
282
+ const offset = options?.offset || 0
283
+ const limit = options?.limit || result.length
284
+ result = result.slice(offset, offset + limit)
285
+
286
+ return result
287
+ }
288
+
289
+ // ==================== Feature Operations ====================
290
+
291
+ /**
292
+ * Add a feature to a product
293
+ */
294
+ async addFeature(productId: string, featureData: Partial<FeatureData>): Promise<FeatureData> {
295
+ const product = products.get(productId)
296
+ if (!product) {
297
+ throw new Error(`Product not found: ${productId}`)
298
+ }
299
+
300
+ if (!featureData.name || !featureData.description) {
301
+ throw new Error('Missing required fields: name and description are required')
302
+ }
303
+
304
+ const now = new Date()
305
+ const feature: FeatureData = {
306
+ id: featureData.id || generateId(),
307
+ name: featureData.name,
308
+ description: featureData.description,
309
+ status: featureData.status || 'draft',
310
+ productId,
311
+ createdAt: now,
312
+ updatedAt: now,
313
+ ...(featureData.metadata !== undefined && { metadata: featureData.metadata }),
314
+ }
315
+
316
+ features.set(feature.id, feature)
317
+ return feature
318
+ }
319
+
320
+ /**
321
+ * Update a feature
322
+ */
323
+ async updateFeature(featureId: string, data: Partial<FeatureData>): Promise<FeatureData> {
324
+ const existing = features.get(featureId)
325
+ if (!existing) {
326
+ throw new Error(`Feature not found: ${featureId}`)
327
+ }
328
+
329
+ const updated: FeatureData = {
330
+ ...existing,
331
+ ...data,
332
+ id: existing.id,
333
+ productId: existing.productId,
334
+ createdAt: existing.createdAt,
335
+ updatedAt: new Date(),
336
+ }
337
+
338
+ features.set(featureId, updated)
339
+ return updated
340
+ }
341
+
342
+ /**
343
+ * Remove a feature
344
+ */
345
+ async removeFeature(featureId: string): Promise<boolean> {
346
+ return features.delete(featureId)
347
+ }
348
+
349
+ /**
350
+ * List features for a product
351
+ */
352
+ async listFeatures(productId: string): Promise<FeatureData[]> {
353
+ return Array.from(features.values()).filter((f) => f.productId === productId)
354
+ }
355
+
356
+ // ==================== Version Operations ====================
357
+
358
+ /**
359
+ * Publish a new version
360
+ */
361
+ async publish(productId: string, versionStr: string, changelog?: string): Promise<VersionData> {
362
+ const product = products.get(productId)
363
+ if (!product) {
364
+ throw new Error(`Product not found: ${productId}`)
365
+ }
366
+
367
+ // Check for duplicate version
368
+ const existingVersion = Array.from(versions.values()).find(
369
+ (v) => v.productId === productId && v.version === versionStr
370
+ )
371
+ if (existingVersion) {
372
+ throw new Error(`Version ${versionStr} already exists for product ${productId}`)
373
+ }
374
+
375
+ const now = new Date()
376
+ const version: VersionData = {
377
+ id: generateId(),
378
+ productId,
379
+ version: versionStr,
380
+ status: 'published',
381
+ createdAt: now,
382
+ publishedAt: now,
383
+ ...(changelog !== undefined && { changelog }),
384
+ }
385
+
386
+ versions.set(version.id, version)
387
+ return version
388
+ }
389
+
390
+ /**
391
+ * List versions for a product
392
+ */
393
+ async listVersions(productId: string): Promise<VersionData[]> {
394
+ return Array.from(versions.values()).filter((v) => v.productId === productId)
395
+ }
396
+
397
+ /**
398
+ * Get a specific version
399
+ */
400
+ async getVersion(productId: string, versionStr: string): Promise<VersionData | null> {
401
+ return (
402
+ Array.from(versions.values()).find(
403
+ (v) => v.productId === productId && v.version === versionStr
404
+ ) || null
405
+ )
406
+ }
407
+
408
+ /**
409
+ * Deprecate a version
410
+ */
411
+ async deprecateVersion(productId: string, versionStr: string): Promise<VersionData> {
412
+ const version = Array.from(versions.values()).find(
413
+ (v) => v.productId === productId && v.version === versionStr
414
+ )
415
+
416
+ if (!version) {
417
+ throw new Error(`Version ${versionStr} not found for product ${productId}`)
418
+ }
419
+
420
+ version.status = 'deprecated'
421
+ versions.set(version.id, version)
422
+ return version
423
+ }
424
+
425
+ // ==================== Bundle Operations ====================
426
+
427
+ /**
428
+ * Compose a bundle from multiple products
429
+ */
430
+ async compose(
431
+ name: string,
432
+ productIds: string[],
433
+ options?: Partial<BundleData>
434
+ ): Promise<BundleData> {
435
+ if (!productIds || productIds.length === 0) {
436
+ throw new Error('Bundle must contain at least one product')
437
+ }
438
+
439
+ // Verify all products exist
440
+ for (const productId of productIds) {
441
+ if (!products.has(productId)) {
442
+ throw new Error(`Product not found: ${productId}`)
443
+ }
444
+ }
445
+
446
+ const now = new Date()
447
+ const bundle: BundleData = {
448
+ id: generateId(),
449
+ name,
450
+ description: options?.description || '',
451
+ productIds,
452
+ createdAt: now,
453
+ updatedAt: now,
454
+ ...(options?.pricing !== undefined && { pricing: options.pricing }),
455
+ ...(options?.metadata !== undefined && { metadata: options.metadata }),
456
+ }
457
+
458
+ bundles.set(bundle.id, bundle)
459
+ return bundle
460
+ }
461
+
462
+ /**
463
+ * Get a bundle by ID
464
+ */
465
+ async getBundle(bundleId: string): Promise<BundleData | null> {
466
+ return bundles.get(bundleId) || null
467
+ }
468
+
469
+ /**
470
+ * Update a bundle
471
+ */
472
+ async updateBundle(bundleId: string, data: Partial<BundleData>): Promise<BundleData> {
473
+ const existing = bundles.get(bundleId)
474
+ if (!existing) {
475
+ throw new Error(`Bundle not found: ${bundleId}`)
476
+ }
477
+
478
+ const updated: BundleData = {
479
+ ...existing,
480
+ ...data,
481
+ id: existing.id,
482
+ createdAt: existing.createdAt,
483
+ updatedAt: new Date(),
484
+ }
485
+
486
+ bundles.set(bundleId, updated)
487
+ return updated
488
+ }
489
+
490
+ /**
491
+ * List all bundles
492
+ */
493
+ async listBundles(): Promise<BundleData[]> {
494
+ return Array.from(bundles.values())
495
+ }
496
+ }
497
+
498
+ // =============================================================================
499
+ // ProductService (WorkerEntrypoint)
500
+ // =============================================================================
501
+
502
+ /**
503
+ * ProductService - WorkerEntrypoint for RPC access
504
+ *
505
+ * Provides `connect()` method that returns a ProductServiceCore instance
506
+ * with all product management operations.
507
+ */
508
+ export class ProductService extends WorkerEntrypoint<Env> {
509
+ // Declare env property from WorkerEntrypoint
510
+ declare env: Env
511
+
512
+ /**
513
+ * Connect and get an RPC-enabled service
514
+ *
515
+ * @returns ProductServiceCore instance for RPC calls
516
+ */
517
+ connect(): ProductServiceCore {
518
+ return new ProductServiceCore(this.env)
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Default export for Cloudflare Workers
524
+ */
525
+ export default ProductService