alepha 0.20.1 → 0.20.2

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 (232) hide show
  1. package/dist/api/files/index.js +2 -1
  2. package/dist/api/files/index.js.map +1 -1
  3. package/dist/api/jobs/index.browser.js +64 -148
  4. package/dist/api/jobs/index.browser.js.map +1 -1
  5. package/dist/api/jobs/index.d.ts +371 -573
  6. package/dist/api/jobs/index.d.ts.map +1 -1
  7. package/dist/api/jobs/index.js +605 -1012
  8. package/dist/api/jobs/index.js.map +1 -1
  9. package/dist/api/notifications/index.d.ts +78 -17
  10. package/dist/api/notifications/index.d.ts.map +1 -1
  11. package/dist/api/notifications/index.js +90 -23
  12. package/dist/api/notifications/index.js.map +1 -1
  13. package/dist/api/payments/index.d.ts +2 -1
  14. package/dist/api/payments/index.d.ts.map +1 -1
  15. package/dist/api/payments/index.js +4 -2
  16. package/dist/api/payments/index.js.map +1 -1
  17. package/dist/api/users/index.d.ts +34 -31
  18. package/dist/api/users/index.d.ts.map +1 -1
  19. package/dist/api/users/index.js +13 -7
  20. package/dist/api/users/index.js.map +1 -1
  21. package/dist/api/verifications/index.js +2 -1
  22. package/dist/api/verifications/index.js.map +1 -1
  23. package/dist/cli/core/index.d.ts +8 -34
  24. package/dist/cli/core/index.d.ts.map +1 -1
  25. package/dist/cli/core/index.js +43 -232
  26. package/dist/cli/core/index.js.map +1 -1
  27. package/dist/cli/platform/index.d.ts +36 -11
  28. package/dist/cli/platform/index.d.ts.map +1 -1
  29. package/dist/cli/platform/index.js +93 -27
  30. package/dist/cli/platform/index.js.map +1 -1
  31. package/dist/command/index.d.ts +1 -1
  32. package/dist/core/index.browser.js +6 -0
  33. package/dist/core/index.browser.js.map +1 -1
  34. package/dist/core/index.d.ts +6 -0
  35. package/dist/core/index.d.ts.map +1 -1
  36. package/dist/core/index.js +6 -0
  37. package/dist/core/index.js.map +1 -1
  38. package/dist/core/index.native.js +6 -0
  39. package/dist/core/index.native.js.map +1 -1
  40. package/dist/core/index.workerd.js +6 -0
  41. package/dist/core/index.workerd.js.map +1 -1
  42. package/dist/react/form/index.d.ts +60 -1
  43. package/dist/react/form/index.d.ts.map +1 -1
  44. package/dist/react/form/index.js +86 -1
  45. package/dist/react/form/index.js.map +1 -1
  46. package/dist/react/head/index.browser.js +16 -1
  47. package/dist/react/head/index.browser.js.map +1 -1
  48. package/dist/react/head/index.d.ts +6 -0
  49. package/dist/react/head/index.d.ts.map +1 -1
  50. package/dist/react/head/index.js +16 -1
  51. package/dist/react/head/index.js.map +1 -1
  52. package/dist/react/router/index.browser.js +0 -10
  53. package/dist/react/router/index.browser.js.map +1 -1
  54. package/dist/react/router/index.d.ts +35 -12
  55. package/dist/react/router/index.d.ts.map +1 -1
  56. package/dist/react/router/index.js +0 -10
  57. package/dist/react/router/index.js.map +1 -1
  58. package/dist/react/ui/index.d.ts +124 -0
  59. package/dist/react/ui/index.d.ts.map +1 -0
  60. package/dist/react/ui/index.js +206 -0
  61. package/dist/react/ui/index.js.map +1 -0
  62. package/dist/router/index.d.ts +13 -13
  63. package/dist/router/index.d.ts.map +1 -1
  64. package/dist/router/index.js +45 -32
  65. package/dist/router/index.js.map +1 -1
  66. package/dist/system/index.d.ts.map +1 -1
  67. package/dist/system/index.js +1 -0
  68. package/dist/system/index.js.map +1 -1
  69. package/dist/topic/core/index.js +1 -1
  70. package/dist/topic/core/index.js.map +1 -1
  71. package/package.json +6 -23
  72. package/src/api/files/jobs/FileJobs.ts +2 -1
  73. package/src/api/jobs/__tests__/$job.spec.ts +316 -2867
  74. package/src/api/jobs/controllers/AdminJobController.ts +29 -138
  75. package/src/api/jobs/entities/jobExecutionEntity.ts +27 -19
  76. package/src/api/jobs/index.browser.ts +5 -7
  77. package/src/api/jobs/index.ts +23 -51
  78. package/src/api/jobs/primitives/$job.ts +66 -58
  79. package/src/api/jobs/providers/JobProvider.ts +561 -566
  80. package/src/api/jobs/providers/JobQueueProvider.ts +18 -19
  81. package/src/api/jobs/schemas/jobConfigAtom.ts +20 -23
  82. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +3 -27
  83. package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +5 -7
  84. package/src/api/jobs/schemas/jobRegistrationSchema.ts +7 -4
  85. package/src/api/jobs/schemas/triggerJobSchema.ts +0 -1
  86. package/src/api/jobs/services/JobService.ts +90 -483
  87. package/src/api/notifications/controllers/AdminNotificationController.ts +19 -12
  88. package/src/api/notifications/index.ts +7 -4
  89. package/src/api/notifications/jobs/NotificationJobs.ts +83 -12
  90. package/src/api/payments/services/PaymentService.ts +4 -2
  91. package/src/api/users/__tests__/UserJobs.spec.ts +10 -49
  92. package/src/api/users/audits/UserAudits.ts +3 -1
  93. package/src/api/users/buckets/UserBuckets.ts +2 -1
  94. package/src/api/users/index.ts +1 -4
  95. package/src/api/users/jobs/UserJobs.ts +5 -4
  96. package/src/api/verifications/jobs/VerificationJobs.ts +2 -1
  97. package/src/cli/core/__tests__/init.spec.ts +1 -1
  98. package/src/cli/core/commands/init.ts +0 -12
  99. package/src/cli/core/services/PackageManagerUtils.ts +2 -9
  100. package/src/cli/core/services/ProjectScaffolder.ts +17 -65
  101. package/src/cli/core/templates/agentMd.ts +2 -8
  102. package/src/cli/core/templates/apiIndexTs.ts +4 -18
  103. package/src/cli/core/templates/mainCss.ts +1 -36
  104. package/src/cli/core/templates/vitestConfigTs.ts +17 -0
  105. package/src/cli/core/templates/webAppRouterTs.ts +2 -85
  106. package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +22 -71
  107. package/src/cli/platform/adapters/CloudflareAdapter.ts +12 -11
  108. package/src/cli/platform/atoms/platformOptions.ts +9 -0
  109. package/src/cli/platform/schemas/cloudflare.ts +3 -2
  110. package/src/cli/platform/services/CloudflareApi.ts +164 -25
  111. package/src/cli/platform/services/WranglerApi.ts +0 -17
  112. package/src/core/Alepha.ts +9 -0
  113. package/src/react/form/index.ts +2 -0
  114. package/src/react/form/services/parseField.ts +163 -0
  115. package/src/react/form/services/prettyName.ts +19 -0
  116. package/src/react/head/providers/BrowserHeadProvider.ts +31 -10
  117. package/src/react/router/primitives/$page.ts +35 -12
  118. package/src/react/ui/atoms/uiAtom.ts +28 -0
  119. package/src/react/ui/components/ColorScheme.tsx +36 -0
  120. package/src/react/ui/hooks/useColorMode.ts +49 -0
  121. package/src/react/ui/hooks/useSidebarState.ts +26 -0
  122. package/src/react/ui/hooks/useTheme.ts +22 -0
  123. package/src/react/ui/index.ts +35 -0
  124. package/src/react/ui/services/UiPersistence.ts +41 -0
  125. package/src/router/TemplatedPathParser.ts +50 -51
  126. package/src/router/__tests__/RouterProvider.spec.ts +62 -0
  127. package/src/router/__tests__/TemplatedPathParser.spec.ts +18 -0
  128. package/src/router/providers/RouterProvider.ts +10 -5
  129. package/src/system/providers/NodeShellProvider.ts +1 -0
  130. package/src/topic/core/providers/TopicProvider.ts +1 -1
  131. package/dist/api/invitations/index.d.ts +0 -790
  132. package/dist/api/invitations/index.d.ts.map +0 -1
  133. package/dist/api/invitations/index.js +0 -662
  134. package/dist/api/invitations/index.js.map +0 -1
  135. package/dist/api/issues/index.d.ts +0 -810
  136. package/dist/api/issues/index.d.ts.map +0 -1
  137. package/dist/api/issues/index.js +0 -444
  138. package/dist/api/issues/index.js.map +0 -1
  139. package/dist/api/subscriptions/index.d.ts +0 -1692
  140. package/dist/api/subscriptions/index.d.ts.map +0 -1
  141. package/dist/api/subscriptions/index.js +0 -1867
  142. package/dist/api/subscriptions/index.js.map +0 -1
  143. package/dist/api/workflows/index.browser.js +0 -246
  144. package/dist/api/workflows/index.browser.js.map +0 -1
  145. package/dist/api/workflows/index.d.ts +0 -1618
  146. package/dist/api/workflows/index.d.ts.map +0 -1
  147. package/dist/api/workflows/index.js +0 -1495
  148. package/dist/api/workflows/index.js.map +0 -1
  149. package/src/api/invitations/__tests__/InvitationService.spec.ts +0 -439
  150. package/src/api/invitations/controllers/AdminInvitationController.ts +0 -86
  151. package/src/api/invitations/controllers/InvitationController.ts +0 -84
  152. package/src/api/invitations/entities/invitations.ts +0 -33
  153. package/src/api/invitations/index.ts +0 -58
  154. package/src/api/invitations/jobs/InvitationJobs.ts +0 -37
  155. package/src/api/invitations/providers/InvitationProvider.ts +0 -45
  156. package/src/api/invitations/schemas/createInvitationSchema.ts +0 -12
  157. package/src/api/invitations/schemas/invitationConfigAtom.ts +0 -20
  158. package/src/api/invitations/schemas/invitationQuerySchema.ts +0 -15
  159. package/src/api/invitations/schemas/invitationResourceSchema.ts +0 -6
  160. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +0 -22
  161. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +0 -10
  162. package/src/api/invitations/services/InvitationService.ts +0 -556
  163. package/src/api/issues/__tests__/IssueService.spec.ts +0 -263
  164. package/src/api/issues/controllers/AdminIssueController.ts +0 -149
  165. package/src/api/issues/controllers/IssueController.ts +0 -44
  166. package/src/api/issues/entities/issues.ts +0 -49
  167. package/src/api/issues/index.ts +0 -50
  168. package/src/api/issues/schemas/createIssueSchema.ts +0 -13
  169. package/src/api/issues/schemas/issueConfigAtom.ts +0 -13
  170. package/src/api/issues/schemas/issueQuerySchema.ts +0 -18
  171. package/src/api/issues/schemas/issueResourceSchema.ts +0 -6
  172. package/src/api/issues/schemas/myIssueQuerySchema.ts +0 -10
  173. package/src/api/issues/schemas/updateIssueSchema.ts +0 -13
  174. package/src/api/issues/services/IssueService.ts +0 -264
  175. package/src/api/jobs/__tests__/$job-middleware.spec.ts +0 -126
  176. package/src/api/jobs/__tests__/JobService.spec.ts +0 -31
  177. package/src/api/jobs/entities/jobExecutionLogEntity.ts +0 -13
  178. package/src/api/jobs/schemas/jobActivitySchema.ts +0 -15
  179. package/src/api/jobs/schemas/jobCronInfoSchema.ts +0 -22
  180. package/src/api/jobs/schemas/jobExecutionDetailResourceSchema.ts +0 -20
  181. package/src/api/jobs/schemas/jobFailureSchema.ts +0 -9
  182. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +0 -14
  183. package/src/api/jobs/schemas/jobStatsSchema.ts +0 -14
  184. package/src/api/jobs/services/JobService-tests.ts +0 -157
  185. package/src/api/subscriptions/__tests__/BillingService.spec.ts +0 -218
  186. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +0 -278
  187. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +0 -212
  188. package/src/api/subscriptions/controllers/SubscriptionController.ts +0 -189
  189. package/src/api/subscriptions/entities/subscriptionEvents.ts +0 -54
  190. package/src/api/subscriptions/entities/subscriptions.ts +0 -68
  191. package/src/api/subscriptions/index.ts +0 -133
  192. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +0 -382
  193. package/src/api/subscriptions/middleware/$requireLimit.ts +0 -50
  194. package/src/api/subscriptions/middleware/$requirePlan.ts +0 -49
  195. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +0 -110
  196. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +0 -8
  197. package/src/api/subscriptions/schemas/changePlanSchema.ts +0 -9
  198. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +0 -11
  199. package/src/api/subscriptions/schemas/entitlementsSchema.ts +0 -21
  200. package/src/api/subscriptions/schemas/mrrSchema.ts +0 -13
  201. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +0 -71
  202. package/src/api/subscriptions/schemas/planResourceSchema.ts +0 -25
  203. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +0 -8
  204. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +0 -19
  205. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +0 -6
  206. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +0 -32
  207. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +0 -23
  208. package/src/api/subscriptions/services/BillingService.ts +0 -437
  209. package/src/api/subscriptions/services/SubscriptionConfig.ts +0 -56
  210. package/src/api/subscriptions/services/SubscriptionService.ts +0 -867
  211. package/src/api/subscriptions/services/UsageService.ts +0 -118
  212. package/src/api/workflows/__tests__/$workflow.spec.ts +0 -616
  213. package/src/api/workflows/controllers/AdminWorkflowController.ts +0 -191
  214. package/src/api/workflows/entities/workflowExecutions.ts +0 -74
  215. package/src/api/workflows/entities/workflowStepExecutions.ts +0 -74
  216. package/src/api/workflows/entities/workflowStepLogs.ts +0 -13
  217. package/src/api/workflows/index.browser.ts +0 -22
  218. package/src/api/workflows/index.ts +0 -115
  219. package/src/api/workflows/jobs/WorkflowJobs.ts +0 -77
  220. package/src/api/workflows/primitives/$workflow.ts +0 -202
  221. package/src/api/workflows/providers/WorkflowProvider.ts +0 -1284
  222. package/src/api/workflows/schemas/workflowActivitySchema.ts +0 -15
  223. package/src/api/workflows/schemas/workflowConfigAtom.ts +0 -51
  224. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +0 -18
  225. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +0 -26
  226. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +0 -30
  227. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +0 -26
  228. package/src/api/workflows/schemas/workflowStatsSchema.ts +0 -16
  229. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +0 -15
  230. package/src/api/workflows/services/WorkflowService.ts +0 -382
  231. package/src/cli/core/templates/apiAppSecurityTs.ts +0 -43
  232. package/src/cli/core/templates/webAdminDashboardTsx.ts +0 -17
@@ -22,9 +22,9 @@ import {
22
22
  cloudflareKVSchema,
23
23
  cloudflareQueueConsumerSchema,
24
24
  cloudflareQueueSchema,
25
- cloudflareR2ListSchema,
25
+ cloudflareR2Schema,
26
26
  cloudflareSecretSchema,
27
- cloudflareVersionListSchema,
27
+ cloudflareVersionSchema,
28
28
  cloudflareWorkerSchema,
29
29
  createD1BodySchema,
30
30
  createHyperdriveBodySchema,
@@ -81,6 +81,16 @@ export class CloudflareApi {
81
81
  this.jurisdiction = jurisdiction;
82
82
  }
83
83
 
84
+ /**
85
+ * Override the Cloudflare account ID (from platform config).
86
+ *
87
+ * When unset, `resolveAccountId` falls back to `CLOUDFLARE_ACCOUNT_ID` env
88
+ * var or the token's single account.
89
+ */
90
+ public setAccountId(accountId?: string): void {
91
+ this.accountId = accountId;
92
+ }
93
+
84
94
  // -------------------------------------------------------------------------
85
95
  // Auth
86
96
  // -------------------------------------------------------------------------
@@ -107,6 +117,12 @@ export class CloudflareApi {
107
117
  return this.accountId;
108
118
  }
109
119
 
120
+ const fromEnv = process.env.CLOUDFLARE_ACCOUNT_ID;
121
+ if (fromEnv) {
122
+ this.accountId = fromEnv;
123
+ return this.accountId;
124
+ }
125
+
110
126
  const res = await this.fetch<CloudflareAccount[]>("/accounts", {
111
127
  schema: t.array(cloudflareAccountSchema),
112
128
  });
@@ -115,6 +131,15 @@ export class CloudflareApi {
115
131
  throw new AlephaError("No Cloudflare accounts found for this token.");
116
132
  }
117
133
 
134
+ if (res.length > 1) {
135
+ const list = res.map((a) => ` - ${a.id} ${a.name}`).join("\n");
136
+ throw new AlephaError(
137
+ `Cloudflare token has access to ${res.length} accounts; set ` +
138
+ `\`CLOUDFLARE_ACCOUNT_ID\` or the \`accountId\` field in your ` +
139
+ `platform config to pick one:\n${list}`,
140
+ );
141
+ }
142
+
118
143
  this.accountId = res[0].id;
119
144
  return this.accountId;
120
145
  }
@@ -125,9 +150,9 @@ export class CloudflareApi {
125
150
 
126
151
  public async listD1(): Promise<CloudflareD1[]> {
127
152
  const accountId = await this.resolveAccountId();
128
- return await this.fetch<CloudflareD1[]>(
153
+ return await this.paginate<CloudflareD1>(
129
154
  `/accounts/${accountId}/d1/database`,
130
- { schema: t.array(cloudflareD1Schema) },
155
+ cloudflareD1Schema,
131
156
  );
132
157
  }
133
158
 
@@ -136,15 +161,19 @@ export class CloudflareApi {
136
161
  location = "weur", // TODO: move to config (or auto-resolve based on account info, or ask ?)
137
162
  ): Promise<CloudflareD1> {
138
163
  const accountId = await this.resolveAccountId();
164
+ // When jurisdiction is set, `primary_location_hint` is silently ignored
165
+ // by the API, so omit it to avoid confusion.
166
+ const body: Record<string, unknown> = { name };
167
+ if (this.jurisdiction) {
168
+ body.jurisdiction = this.jurisdiction;
169
+ } else {
170
+ body.primary_location_hint = location;
171
+ }
139
172
  return await this.fetch<CloudflareD1>(
140
173
  `/accounts/${accountId}/d1/database`,
141
174
  {
142
175
  method: "POST",
143
- body: {
144
- name,
145
- primary_location_hint: location,
146
- ...(this.jurisdiction ? { jurisdiction: this.jurisdiction } : {}),
147
- },
176
+ body,
148
177
  bodySchema: createD1BodySchema,
149
178
  schema: cloudflareD1Schema,
150
179
  },
@@ -164,9 +193,10 @@ export class CloudflareApi {
164
193
 
165
194
  public async listKV(): Promise<CloudflareKV[]> {
166
195
  const accountId = await this.resolveAccountId();
167
- return await this.fetch<CloudflareKV[]>(
196
+ return await this.paginate<CloudflareKV>(
168
197
  `/accounts/${accountId}/storage/kv/namespaces`,
169
- { schema: t.array(cloudflareKVSchema) },
198
+ cloudflareKVSchema,
199
+ 100, // KV list caps at 100 per page
170
200
  );
171
201
  }
172
202
 
@@ -197,11 +227,11 @@ export class CloudflareApi {
197
227
 
198
228
  public async listR2(): Promise<CloudflareR2[]> {
199
229
  const accountId = await this.resolveAccountId();
200
- const res = await this.fetch<{ buckets: CloudflareR2[] }>(
230
+ return await this.paginateCursor<CloudflareR2>(
201
231
  `/accounts/${accountId}/r2/buckets`,
202
- { schema: cloudflareR2ListSchema },
232
+ "buckets",
233
+ cloudflareR2Schema,
203
234
  );
204
- return res.buckets;
205
235
  }
206
236
 
207
237
  public async createR2(name: string): Promise<void> {
@@ -226,9 +256,9 @@ export class CloudflareApi {
226
256
 
227
257
  public async listQueues(): Promise<CloudflareQueue[]> {
228
258
  const accountId = await this.resolveAccountId();
229
- return await this.fetch<CloudflareQueue[]>(
259
+ return await this.paginate<CloudflareQueue>(
230
260
  `/accounts/${accountId}/queues`,
231
- { schema: t.array(cloudflareQueueSchema) },
261
+ cloudflareQueueSchema,
232
262
  );
233
263
  }
234
264
 
@@ -253,9 +283,9 @@ export class CloudflareApi {
253
283
  queueId: string,
254
284
  ): Promise<CloudflareQueueConsumer[]> {
255
285
  const accountId = await this.resolveAccountId();
256
- return await this.fetch<CloudflareQueueConsumer[]>(
286
+ return await this.paginate<CloudflareQueueConsumer>(
257
287
  `/accounts/${accountId}/queues/${queueId}/consumers`,
258
- { schema: t.array(cloudflareQueueConsumerSchema) },
288
+ cloudflareQueueConsumerSchema,
259
289
  );
260
290
  }
261
291
 
@@ -264,8 +294,13 @@ export class CloudflareApi {
264
294
  consumerService: string,
265
295
  ): Promise<void> {
266
296
  const accountId = await this.resolveAccountId();
297
+ const consumers = await this.listQueueConsumers(queueId);
298
+ const consumer = consumers.find((c) => c.service === consumerService);
299
+ if (!consumer) {
300
+ return;
301
+ }
267
302
  await this.fetch(
268
- `/accounts/${accountId}/queues/${queueId}/consumers/${consumerService}`,
303
+ `/accounts/${accountId}/queues/${queueId}/consumers/${consumer.consumer_id}`,
269
304
  { method: "DELETE" },
270
305
  );
271
306
  }
@@ -276,9 +311,9 @@ export class CloudflareApi {
276
311
 
277
312
  public async listHyperdrive(): Promise<CloudflareHyperdrive[]> {
278
313
  const accountId = await this.resolveAccountId();
279
- return await this.fetch<CloudflareHyperdrive[]>(
314
+ return await this.paginate<CloudflareHyperdrive>(
280
315
  `/accounts/${accountId}/hyperdrive/configs`,
281
- { schema: t.array(cloudflareHyperdriveSchema) },
316
+ cloudflareHyperdriveSchema,
282
317
  );
283
318
  }
284
319
 
@@ -338,20 +373,22 @@ export class CloudflareApi {
338
373
  scriptName: string,
339
374
  ): Promise<CloudflareDeployment[]> {
340
375
  const accountId = await this.resolveAccountId();
376
+ // Deployments list is wrapped in `{ deployments }` and returns newest
377
+ // first; for picking the active deployment we only need the top page.
341
378
  const res = await this.fetch<{ deployments: CloudflareDeployment[] }>(
342
379
  `/accounts/${accountId}/workers/scripts/${scriptName}/deployments`,
343
- { schema: cloudflareDeploymentListSchema },
380
+ { schema: cloudflareDeploymentListSchema, query: { per_page: "100" } },
344
381
  );
345
382
  return res.deployments;
346
383
  }
347
384
 
348
385
  public async listVersions(scriptName: string): Promise<CloudflareVersion[]> {
349
386
  const accountId = await this.resolveAccountId();
350
- const res = await this.fetch<{ items: CloudflareVersion[] }>(
387
+ return await this.paginateCursor<CloudflareVersion>(
351
388
  `/accounts/${accountId}/workers/scripts/${scriptName}/versions`,
352
- { schema: cloudflareVersionListSchema },
389
+ "items",
390
+ cloudflareVersionSchema,
353
391
  );
354
- return res.items;
355
392
  }
356
393
 
357
394
  // -------------------------------------------------------------------------
@@ -428,6 +465,13 @@ export class CloudflareApi {
428
465
  success: boolean;
429
466
  result: T;
430
467
  errors: CloudflareApiError[];
468
+ result_info?: {
469
+ page: number;
470
+ per_page: number;
471
+ total_pages?: number;
472
+ count?: number;
473
+ total_count?: number;
474
+ };
431
475
  };
432
476
 
433
477
  if (!json.success) {
@@ -444,6 +488,101 @@ export class CloudflareApi {
444
488
  return json.result;
445
489
  }
446
490
 
491
+ /**
492
+ * Paginate a page-based list endpoint (`result_info.total_pages`).
493
+ *
494
+ * Cloudflare defaults to `per_page=20`; we push it to 1000 (max on most
495
+ * list endpoints) and loop if more pages exist. Each page is validated
496
+ * against the item schema.
497
+ */
498
+ protected async paginate<T>(
499
+ path: string,
500
+ itemSchema: TSchema,
501
+ perPage = 1000,
502
+ ): Promise<T[]> {
503
+ const results: T[] = [];
504
+ let page = 1;
505
+
506
+ while (true) {
507
+ const token = await this.resolveToken();
508
+ const url = `${CloudflareApi.BASE}${path}?per_page=${perPage}&page=${page}`;
509
+
510
+ const headers: Record<string, string> = {
511
+ Authorization: `Bearer ${token}`,
512
+ };
513
+ if (this.jurisdiction && /\/r2\//.test(path)) {
514
+ headers["cf-r2-jurisdiction"] = this.jurisdiction;
515
+ }
516
+
517
+ const response = await globalThis.fetch(url, { method: "GET", headers });
518
+ const json = (await response.json()) as {
519
+ success: boolean;
520
+ result: T[];
521
+ errors: CloudflareApiError[];
522
+ result_info?: { page: number; total_pages?: number };
523
+ };
524
+
525
+ if (!json.success) {
526
+ const messages = json.errors.map((e) => e.message).join(", ");
527
+ throw new AlephaError(
528
+ `Cloudflare API error (GET ${path}): ${messages}`,
529
+ );
530
+ }
531
+
532
+ const validated = this.alepha.codec.validate(
533
+ t.array(itemSchema),
534
+ json.result,
535
+ ) as T[];
536
+ results.push(...validated);
537
+
538
+ const totalPages = json.result_info?.total_pages;
539
+ if (!totalPages || page >= totalPages || validated.length === 0) {
540
+ break;
541
+ }
542
+ page++;
543
+ }
544
+
545
+ return results;
546
+ }
547
+
548
+ /**
549
+ * Paginate a cursor-based list endpoint where `result` is an object
550
+ * containing both the items array and a `cursor` field (R2 buckets,
551
+ * Workers versions). Returns the flattened item array.
552
+ */
553
+ protected async paginateCursor<T>(
554
+ path: string,
555
+ itemsKey: string,
556
+ itemSchema: TSchema,
557
+ perPage = 1000,
558
+ ): Promise<T[]> {
559
+ const results: T[] = [];
560
+ let cursor: string | undefined;
561
+
562
+ while (true) {
563
+ const query: Record<string, string> = { per_page: String(perPage) };
564
+ if (cursor) {
565
+ query.cursor = cursor;
566
+ }
567
+
568
+ const res = await this.fetch<Record<string, unknown>>(path, { query });
569
+ const items = (res[itemsKey] as unknown[]) ?? [];
570
+ const validated = this.alepha.codec.validate(
571
+ t.array(itemSchema),
572
+ items,
573
+ ) as T[];
574
+ results.push(...validated);
575
+
576
+ const nextCursor = res.cursor as string | undefined;
577
+ if (!nextCursor || validated.length === 0) {
578
+ break;
579
+ }
580
+ cursor = nextCursor;
581
+ }
582
+
583
+ return results;
584
+ }
585
+
447
586
  // -------------------------------------------------------------------------
448
587
  // Helpers
449
588
  // -------------------------------------------------------------------------
@@ -124,21 +124,4 @@ export class WranglerApi {
124
124
  { resolve: true, env: { CI: "1" } },
125
125
  );
126
126
  }
127
-
128
- // -------------------------------------------------------------------------
129
- // Secrets
130
- // -------------------------------------------------------------------------
131
-
132
- /**
133
- * Push secrets in bulk to a worker.
134
- */
135
- public async secretBulk(
136
- secretsPath: string,
137
- workerName: string,
138
- ): Promise<void> {
139
- await this.runShell(
140
- `wrangler secret bulk ${secretsPath} --name=${workerName}`,
141
- { resolve: true },
142
- );
143
- }
144
127
  }
@@ -826,6 +826,15 @@ export class Alepha {
826
826
  return this;
827
827
  }
828
828
 
829
+ /**
830
+ * Alias for {@link Alepha#with}.
831
+ */
832
+ public register<T extends object>(
833
+ serviceEntry: ServiceEntry<T> | { default: ServiceEntry<T> },
834
+ ): this {
835
+ return this.with(serviceEntry);
836
+ }
837
+
829
838
  /**
830
839
  * Get an instance of the specified service from the container.
831
840
  *
@@ -9,6 +9,8 @@ export * from "./hooks/useForm.ts";
9
9
  export * from "./hooks/useFormState.ts";
10
10
  export * from "./hooks/useFormValues.ts";
11
11
  export * from "./services/FormModel.ts";
12
+ export * from "./services/parseField.ts";
13
+ export * from "./services/prettyName.ts";
12
14
 
13
15
  // ---------------------------------------------------------------------------------------------------------------------
14
16
 
@@ -0,0 +1,163 @@
1
+ import { type TSchema, TypeBoxError } from "alepha";
2
+ import type { BaseInputField } from "./FormModel.ts";
3
+ import { prettyName } from "./prettyName.ts";
4
+
5
+ /**
6
+ * Semantic icon hint derived from schema metadata. UI layers map this
7
+ * to their own icon set — this module is headless and ships no JSX.
8
+ */
9
+ export type IconHint =
10
+ | "email"
11
+ | "password"
12
+ | "phone"
13
+ | "url"
14
+ | "number"
15
+ | "calendar"
16
+ | "clock"
17
+ | "list"
18
+ | "text"
19
+ | "user"
20
+ | "file"
21
+ | "switch";
22
+
23
+ export interface FieldConstraints {
24
+ minLength?: number;
25
+ maxLength?: number;
26
+ minimum?: number;
27
+ maximum?: number;
28
+ pattern?: string;
29
+ }
30
+
31
+ export interface FieldMeta {
32
+ id?: string;
33
+ label: string;
34
+ description?: string;
35
+ error?: string;
36
+ required: boolean;
37
+ type?: string;
38
+ format?: string;
39
+ isEnum: boolean;
40
+ isArray: boolean;
41
+ isObject: boolean;
42
+ isArrayOfObjects: boolean;
43
+ enum?: readonly unknown[];
44
+ iconHint?: IconHint;
45
+ constraints: FieldConstraints;
46
+ testId?: string;
47
+ schema: TSchema;
48
+ }
49
+
50
+ export interface ParseFieldOptions {
51
+ label?: string;
52
+ description?: string;
53
+ error?: Error;
54
+ }
55
+
56
+ /**
57
+ * Derives a {@link FieldMeta} from an `InputField` (from `useForm`) plus
58
+ * optional overrides. Pure — no React, no JSX, no UI library coupling.
59
+ *
60
+ * UI components consume this metadata to render labels, descriptions,
61
+ * error messages, icons, and validation constraints.
62
+ */
63
+ export const parseField = (
64
+ input: BaseInputField,
65
+ options: ParseFieldOptions = {},
66
+ ): FieldMeta => {
67
+ const schema = input.schema as TSchema & {
68
+ type?: string;
69
+ format?: string;
70
+ title?: string;
71
+ description?: string;
72
+ enum?: readonly unknown[];
73
+ minLength?: number;
74
+ maxLength?: number;
75
+ minimum?: number;
76
+ maximum?: number;
77
+ pattern?: string;
78
+ properties?: unknown;
79
+ items?: { properties?: unknown };
80
+ };
81
+
82
+ const label =
83
+ options.label ??
84
+ (typeof schema.title === "string" ? schema.title : undefined) ??
85
+ prettyName(input.path);
86
+
87
+ const description =
88
+ options.description ??
89
+ (typeof schema.description === "string" ? schema.description : undefined);
90
+
91
+ const error =
92
+ options.error instanceof TypeBoxError
93
+ ? (options.error as TypeBoxError).value?.message
94
+ : undefined;
95
+
96
+ const type = schema.type;
97
+ const format = typeof schema.format === "string" ? schema.format : undefined;
98
+ const isEnum = Array.isArray(schema.enum);
99
+ const isArray = type === "array";
100
+ const isObject = type === "object" && Boolean(schema.properties);
101
+ const isArrayOfObjects =
102
+ isArray && Boolean(schema.items && (schema.items as any).properties);
103
+
104
+ const name = input.props.name;
105
+ const iconHint = inferIconHint({ type, format, name, isEnum, isArray });
106
+
107
+ const constraints: FieldConstraints = {};
108
+ if (typeof schema.minLength === "number")
109
+ constraints.minLength = schema.minLength;
110
+ if (typeof schema.maxLength === "number")
111
+ constraints.maxLength = schema.maxLength;
112
+ if (typeof schema.minimum === "number") constraints.minimum = schema.minimum;
113
+ if (typeof schema.maximum === "number") constraints.maximum = schema.maximum;
114
+ if (typeof schema.pattern === "string") constraints.pattern = schema.pattern;
115
+
116
+ return {
117
+ id: input.props.id,
118
+ label,
119
+ description,
120
+ error,
121
+ required: input.required,
122
+ type,
123
+ format,
124
+ isEnum,
125
+ isArray,
126
+ isObject,
127
+ isArrayOfObjects,
128
+ enum: schema.enum,
129
+ iconHint,
130
+ constraints,
131
+ testId: (input.props as Record<string, unknown>)["data-testid"] as
132
+ | string
133
+ | undefined,
134
+ schema: input.schema,
135
+ };
136
+ };
137
+
138
+ const inferIconHint = (params: {
139
+ type?: string;
140
+ format?: string;
141
+ name?: string;
142
+ isEnum: boolean;
143
+ isArray: boolean;
144
+ }): IconHint | undefined => {
145
+ const { type, format, name, isEnum, isArray } = params;
146
+
147
+ if (format === "email") return "email";
148
+ if (format === "url" || format === "uri") return "url";
149
+ if (format === "tel" || format === "phone") return "phone";
150
+ if (format === "date" || format === "date-time") return "calendar";
151
+ if (format === "time") return "clock";
152
+
153
+ if (name?.toLowerCase().includes("password")) return "password";
154
+ if (name?.toLowerCase().includes("email")) return "email";
155
+ if (name?.toLowerCase().includes("phone")) return "phone";
156
+
157
+ if (type === "boolean") return "switch";
158
+ if (type === "number" || type === "integer") return "number";
159
+ if (isEnum || isArray) return "list";
160
+ if (type === "string") return "text";
161
+
162
+ return undefined;
163
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Converts a path or identifier string into a pretty display name.
3
+ * For paths like "/contacts/0/name", extracts just the field name "Name".
4
+ * Handles camelCase and snake_case conversion to Title Case.
5
+ *
6
+ * @example
7
+ * prettyName("/userName") // "User Name"
8
+ * prettyName("/contacts/0/email") // "Email"
9
+ * prettyName("/address/streetName") // "Street Name"
10
+ * prettyName("first_name") // "First Name"
11
+ */
12
+ export const prettyName = (name: string): string => {
13
+ const segments = name.split("/").filter((s) => s && !/^\d+$/.test(s));
14
+ const fieldName = segments[segments.length - 1] || name.replaceAll("/", "");
15
+ return fieldName
16
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
17
+ .replace(/_/g, " ")
18
+ .replace(/\b\w/g, (c) => c.toUpperCase());
19
+ };
@@ -154,10 +154,12 @@ export class BrowserHeadProvider {
154
154
  | string
155
155
  | (Record<string, string | boolean | undefined> & { content?: string }),
156
156
  ): void {
157
- const el = document.createElement("script");
158
-
159
- // Handle plain string as inline script
157
+ // Plain string → inline script. Dedupe by exact content match against
158
+ // any existing inline script (handles SSR-emitted globals that would
159
+ // otherwise be re-appended on hydration).
160
160
  if (typeof script === "string") {
161
+ if (this.findInlineScriptByContent(document, script)) return;
162
+ const el = document.createElement("script");
161
163
  el.textContent = script;
162
164
  document.head.appendChild(el);
163
165
  return;
@@ -165,14 +167,18 @@ export class BrowserHeadProvider {
165
167
 
166
168
  const { content, ...attrs } = script;
167
169
 
168
- // For scripts with src, check if already exists
170
+ // src-based scripts: dedupe by src attribute (existing behaviour).
169
171
  if (attrs.src) {
170
- const existing = document.querySelector(`script[src="${attrs.src}"]`);
171
- if (existing) {
172
- return;
173
- }
172
+ if (document.querySelector(`script[src="${attrs.src}"]`)) return;
173
+ } else if (typeof attrs.id === "string") {
174
+ // id-based dedupe — single source of truth per id.
175
+ if (document.querySelector(`script#${CSS.escape(attrs.id)}`)) return;
176
+ } else if (content) {
177
+ // Inline scripts with `content` and no src/id: fall back to content match.
178
+ if (this.findInlineScriptByContent(document, content)) return;
174
179
  }
175
180
 
181
+ const el = document.createElement("script");
176
182
  for (const [key, value] of Object.entries(attrs)) {
177
183
  if (value === true) {
178
184
  el.setAttribute(key, "");
@@ -180,14 +186,29 @@ export class BrowserHeadProvider {
180
186
  el.setAttribute(key, String(value));
181
187
  }
182
188
  }
183
-
184
189
  if (content) {
185
190
  el.textContent = content;
186
191
  }
187
-
188
192
  document.head.appendChild(el);
189
193
  }
190
194
 
195
+ /**
196
+ * Find an existing inline `<script>` tag (no `src`) with matching textContent.
197
+ * Used to make `renderScriptTag` idempotent across hydration + navigation,
198
+ * so SSR-emitted global scripts aren't re-appended client-side.
199
+ */
200
+ protected findInlineScriptByContent(
201
+ document: Document,
202
+ content: string,
203
+ ): Element | null {
204
+ for (const existing of document.head.querySelectorAll(
205
+ "script:not([src])",
206
+ )) {
207
+ if (existing.textContent === content) return existing;
208
+ }
209
+ return null;
210
+ }
211
+
191
212
  protected renderMetaTag(document: Document, meta: HeadMeta): void {
192
213
  const { content } = meta;
193
214
 
@@ -50,16 +50,6 @@ import { ReactPageService } from "../services/ReactPageService.ts";
50
50
  * - Hierarchical error handling (child → parent)
51
51
  * - HTTP status code handling (404, 401, etc.)
52
52
  *
53
- * **Page Animations**
54
- * - CSS-based enter/exit animations
55
- * - Dynamic animations based on page state
56
- * - Custom timing and easing functions
57
- *
58
- * **Lifecycle Management**
59
- * - Server response hooks for headers and status codes
60
- * - Page leave handlers for cleanup (browser only)
61
- * - Permission-based access control
62
- *
63
53
  * @example Simple page with data fetching
64
54
  * ```typescript
65
55
  * const userProfile = $page({
@@ -202,13 +192,46 @@ export interface PagePrimitiveOptions<
202
192
  lazy?: () => Promise<{ default: FC<TProps & TPropsParent> }>;
203
193
 
204
194
  /**
205
- * Attach child pages to create nested routes.
206
- * This will make the page a parent route.
195
+ * Attach child pages to create nested routes, adopting them as children of
196
+ * this page.
197
+ *
198
+ * Use this when you want a parent to own children it cannot modify — most
199
+ * notably pages that come from an injected router in another package, whose
200
+ * `$page` definitions are frozen and cannot declare `parent` themselves.
201
+ *
202
+ * ```ts
203
+ * layout = $page({
204
+ * path: "/app",
205
+ * children: () => [
206
+ * this.productRouter.catalogPage, // from $inject(ProductRouter)
207
+ * this.productRouter.checkoutPage,
208
+ * ],
209
+ * });
210
+ * ```
211
+ *
212
+ * Use a thunk (`() => [...]`) when the children are defined later in the
213
+ * same class.
214
+ *
215
+ * **Declare each edge from one side only.** If a child already sets
216
+ * `parent: thisPage`, do NOT also add it to `children` — the link is
217
+ * already established, and declaring it on both sides creates a TypeScript
218
+ * circular dependency between the two class fields (each references the
219
+ * other before it is initialised).
207
220
  */
208
221
  children?: Array<PagePrimitive> | (() => Array<PagePrimitive>);
209
222
 
210
223
  /**
211
224
  * Define a parent page for nested routing.
225
+ *
226
+ * Use this when you own the child page and can edit its definition — it is
227
+ * the simplest way to nest routes and reads top-down. For pages you do NOT
228
+ * own (e.g. pages exposed by an injected router from another package), let
229
+ * the parent adopt them via its `children` option instead.
230
+ *
231
+ * **Declare each edge from one side only.** If you set `parent` here, do
232
+ * NOT also add this page to the parent's `children` array — the link is
233
+ * already established, and declaring it on both sides creates a TypeScript
234
+ * circular dependency between the two class fields.
212
235
  */
213
236
  parent?: PagePrimitive<PageConfigSchema, TPropsParent, any>;
214
237