@rebasepro/server 0.24.0 → 0.24.1-canary.gcb918fc

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 (224) hide show
  1. package/README.md +27 -2
  2. package/dist/GCSStorageController-fv6JAZ87.js +402 -0
  3. package/dist/GCSStorageController-fv6JAZ87.js.map +1 -0
  4. package/dist/S3StorageController-BNg-pXiY.js +480 -0
  5. package/dist/S3StorageController-BNg-pXiY.js.map +1 -0
  6. package/dist/api/mount-path.d.ts +30 -0
  7. package/dist/api/mount.d.ts +6 -13
  8. package/dist/api/rest/api-generator.d.ts +53 -1
  9. package/dist/api/rest/auth-collection-writes.d.ts +28 -21
  10. package/dist/api/rest/field-access-query.d.ts +28 -8
  11. package/dist/api/rest/field-ops.d.ts +19 -7
  12. package/dist/api/rest/query-body.d.ts +10 -0
  13. package/dist/api/rest/write-validation.d.ts +7 -5
  14. package/dist/api/schema-editor-routes.d.ts +10 -1
  15. package/dist/api/sql-export-routes.d.ts +8 -0
  16. package/dist/api/types.d.ts +35 -0
  17. package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-mCeSC03x.js} +3 -3
  18. package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-mCeSC03x.js.map} +1 -1
  19. package/dist/auth/access.d.ts +57 -8
  20. package/dist/auth/admin-user-ops.d.ts +24 -2
  21. package/dist/auth/admin-users-route.d.ts +1 -0
  22. package/dist/auth/api-keys/api-key-middleware.d.ts +10 -3
  23. package/dist/auth/api-keys/api-key-routes.d.ts +4 -1
  24. package/dist/auth/builtin-auth-adapter.d.ts +13 -0
  25. package/dist/auth/email-change-routes.d.ts +3 -0
  26. package/dist/auth/expired-token-sweep.d.ts +5 -1
  27. package/dist/auth/interfaces.d.ts +10 -0
  28. package/dist/auth/jwks-routes.d.ts +5 -1
  29. package/dist/auth/jwt-crypto.d.ts +7 -0
  30. package/dist/auth/jwt-keys.d.ts +4 -0
  31. package/dist/auth/jwt.d.ts +282 -166
  32. package/dist/auth/magic-link-routes.d.ts +3 -0
  33. package/dist/auth/mfa-crypto.d.ts +9 -3
  34. package/dist/auth/mfa-gate.d.ts +4 -2
  35. package/dist/auth/mfa-routes.d.ts +6 -0
  36. package/dist/auth/middleware.d.ts +9 -2
  37. package/dist/auth/otp-routes.d.ts +3 -0
  38. package/dist/auth/rate-limiter.d.ts +61 -34
  39. package/dist/auth/registration-policy.d.ts +17 -0
  40. package/dist/auth/require-auth.d.ts +1 -1
  41. package/dist/auth/routes.d.ts +16 -0
  42. package/dist/auth/session-routes.d.ts +6 -0
  43. package/dist/auth/user-picking.d.ts +37 -0
  44. package/dist/auth/verify-credential.d.ts +25 -4
  45. package/dist/{auth-DMLngxn_.js → auth-GnONzteu.js} +2869 -1871
  46. package/dist/auth-GnONzteu.js.map +1 -0
  47. package/dist/auth_adapter-CIItCTc8.js +21 -0
  48. package/dist/auth_adapter-CIItCTc8.js.map +1 -0
  49. package/dist/backend/create-backend.d.ts +281 -0
  50. package/dist/backend/default-backend.d.ts +35 -0
  51. package/dist/backend/options.d.ts +912 -0
  52. package/dist/backend/scope.d.ts +23 -0
  53. package/dist/backend/services.d.ts +54 -0
  54. package/dist/backend-DTAOsLQc.js.map +1 -1
  55. package/dist/backup/backup-common.d.ts +14 -3
  56. package/dist/backup/backup-routes.d.ts +23 -2
  57. package/dist/backup/backup-storage.d.ts +3 -1
  58. package/dist/backup/index.d.ts +3 -1
  59. package/dist/{backup-CN0s50D2.js → backup-BnvkSLHi.js} +176 -35
  60. package/dist/backup-BnvkSLHi.js.map +1 -0
  61. package/dist/boot/boot.d.ts +2 -2
  62. package/dist/boot/driver.d.ts +3 -0
  63. package/dist/boot/env.d.ts +9 -2
  64. package/dist/boot/http-server.d.ts +22 -0
  65. package/dist/boot/options.d.ts +16 -1
  66. package/dist/boot/provision.d.ts +4 -2
  67. package/dist/boot/runtime-cors.d.ts +17 -0
  68. package/dist/boot/sources.d.ts +2 -0
  69. package/dist/boot/static-routing.d.ts +9 -0
  70. package/dist/{contract-routes-fz8i4pxs.js → contract-routes-Ey6ftnKH.js} +4 -20
  71. package/dist/contract-routes-Ey6ftnKH.js.map +1 -0
  72. package/dist/cron/cron-loader.d.ts +77 -18
  73. package/dist/cron/cron-routes.d.ts +1 -1
  74. package/dist/cron/cron-scheduler.d.ts +69 -3
  75. package/dist/cron/cron-store.d.ts +33 -5
  76. package/dist/cron/define-cron.d.ts +6 -2
  77. package/dist/cron/index.d.ts +3 -3
  78. package/dist/cron-loader-B-u0snqq.js +202 -0
  79. package/dist/cron-loader-B-u0snqq.js.map +1 -0
  80. package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-OD6BstUj.js} +27 -4
  81. package/dist/cron-routes-OD6BstUj.js.map +1 -0
  82. package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-idtLPoN-.js} +299 -33
  83. package/dist/cron-scheduler-idtLPoN-.js.map +1 -0
  84. package/dist/cron-store-CZ30ZBi-.js +577 -0
  85. package/dist/cron-store-CZ30ZBi-.js.map +1 -0
  86. package/dist/email/smtp-email-service.d.ts +23 -0
  87. package/dist/email/types.d.ts +18 -0
  88. package/dist/env.d.ts +0 -37
  89. package/dist/filter-dialect-O1HK6Nvr.js +1628 -0
  90. package/dist/filter-dialect-O1HK6Nvr.js.map +1 -0
  91. package/dist/{function-loader-D7o5Epjj.js → function-loader-iKhOZatS.js} +68 -38
  92. package/dist/function-loader-iKhOZatS.js.map +1 -0
  93. package/dist/{function-routes-CaNG4waN.js → function-routes-j9TwpvQe.js} +5 -4
  94. package/dist/function-routes-j9TwpvQe.js.map +1 -0
  95. package/dist/functions/context.d.ts +9 -0
  96. package/dist/functions/define-function.d.ts +10 -6
  97. package/dist/functions/function-loader.d.ts +75 -18
  98. package/dist/functions/function-routes.d.ts +2 -1
  99. package/dist/functions/index.d.ts +5 -4
  100. package/dist/functions/index.js +42 -13
  101. package/dist/functions/index.js.map +1 -1
  102. package/dist/functions/internal.d.ts +2 -2
  103. package/dist/functions/proxy.d.ts +6 -2
  104. package/dist/functions/request-timeout.d.ts +7 -5
  105. package/dist/history/history-routes.d.ts +9 -0
  106. package/dist/history/retention-sweep.d.ts +81 -0
  107. package/dist/{history-recorder-Nr8zLvoU.js → history-recorder-BfWRwmPy.js} +3 -3
  108. package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-BfWRwmPy.js.map} +1 -1
  109. package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
  110. package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
  111. package/dist/index.d.ts +23 -10
  112. package/dist/index.es.js +24423 -20038
  113. package/dist/index.es.js.map +1 -1
  114. package/dist/init/docs.d.ts +27 -10
  115. package/dist/init/process-safety.d.ts +7 -2
  116. package/dist/init/shutdown.d.ts +18 -3
  117. package/dist/init.d.ts +49 -865
  118. package/dist/internal-tables-L2t39RdT.js +136 -0
  119. package/dist/internal-tables-L2t39RdT.js.map +1 -0
  120. package/dist/jobs/job-queue.d.ts +8 -0
  121. package/dist/jobs/job-store.d.ts +24 -0
  122. package/dist/jobs/types.d.ts +25 -4
  123. package/dist/{jobs-DqYNfquG.js → jobs-BCbflZj2.js} +234 -24
  124. package/dist/jobs-BCbflZj2.js.map +1 -0
  125. package/dist/{jwt-R6bSPMjk.js → jwt-BlvuKoR6.js} +529 -454
  126. package/dist/jwt-BlvuKoR6.js.map +1 -0
  127. package/dist/{keys-GAVZqbqx.js → keys-COzQ2aE8.js} +2 -2
  128. package/dist/{keys-GAVZqbqx.js.map → keys-COzQ2aE8.js.map} +1 -1
  129. package/dist/load-failure-L5hvfDoG.js +80 -0
  130. package/dist/load-failure-L5hvfDoG.js.map +1 -0
  131. package/dist/mcp/mcp-routes.d.ts +6 -0
  132. package/dist/mcp/mcp-tools.d.ts +8 -0
  133. package/dist/mcp/oauth-routes.d.ts +7 -0
  134. package/dist/metrics/index.d.ts +18 -0
  135. package/dist/metrics/pool-wait.d.ts +50 -0
  136. package/dist/offline-QNQgsPOq-B2yGeXkn.js +3305 -0
  137. package/dist/offline-QNQgsPOq-B2yGeXkn.js.map +1 -0
  138. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js +1915 -0
  139. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js.map +1 -0
  140. package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-D9zUQmEp.js} +366 -82
  141. package/dist/openapi-generator-D9zUQmEp.js.map +1 -0
  142. package/dist/policy-PdzX-Opd.js +121 -0
  143. package/dist/policy-PdzX-Opd.js.map +1 -0
  144. package/dist/project_manifest-R6y_KHms.js +59 -0
  145. package/dist/project_manifest-R6y_KHms.js.map +1 -0
  146. package/dist/{proxy-qRlqeUmO.js → proxy-CkTlMGmh.js} +4 -3
  147. package/dist/proxy-CkTlMGmh.js.map +1 -0
  148. package/dist/{query-parser-BgiKJKvc.js → query-parser-4WkMoA_K.js} +510 -15
  149. package/dist/query-parser-4WkMoA_K.js.map +1 -0
  150. package/dist/{request-timeout-DgH7j8qO.js → request-timeout-Dnl2_OBc.js} +15 -8
  151. package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
  152. package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
  153. package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
  154. package/dist/schema-edit/apply-schema-change.d.ts +10 -0
  155. package/dist/schema-edit/local-source.d.ts +29 -0
  156. package/dist/schema-edit/remote-source.d.ts +13 -3
  157. package/dist/schema-edit/source-editor.d.ts +28 -0
  158. package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-C11Tumyd.js} +54 -10
  159. package/dist/schema-editor-routes-C11Tumyd.js.map +1 -0
  160. package/dist/services/outbound-post.d.ts +50 -0
  161. package/dist/services/outbound-url-guard.d.ts +34 -5
  162. package/dist/services/routed-realtime-service.d.ts +5 -0
  163. package/dist/services/webhook-service.d.ts +66 -14
  164. package/dist/singleton.d.ts +9 -5
  165. package/dist/soft-delete-params-DfV3vVN5.js +668 -0
  166. package/dist/soft-delete-params-DfV3vVN5.js.map +1 -0
  167. package/dist/sql-export-routes-ClRwzIsC.js +240 -0
  168. package/dist/sql-export-routes-ClRwzIsC.js.map +1 -0
  169. package/dist/{src-CatHFUym.js → src-D5Y3N-S4.js} +2 -1
  170. package/dist/{src-CatHFUym.js.map → src-D5Y3N-S4.js.map} +1 -1
  171. package/dist/storage/GCSStorageController.d.ts +46 -2
  172. package/dist/storage/LocalStorageController.d.ts +67 -2
  173. package/dist/storage/S3StorageController.d.ts +37 -3
  174. package/dist/storage/image-transform.d.ts +5 -0
  175. package/dist/storage/maintenance-routes.d.ts +23 -0
  176. package/dist/storage/maintenance.d.ts +126 -0
  177. package/dist/storage/rendition-cache.d.ts +9 -11
  178. package/dist/storage/request-principals.d.ts +30 -0
  179. package/dist/storage/routes.d.ts +20 -0
  180. package/dist/storage/source-variables.d.ts +10 -0
  181. package/dist/storage/streams.d.ts +42 -0
  182. package/dist/storage/tus-handler.d.ts +46 -2
  183. package/dist/storage/types.d.ts +82 -7
  184. package/dist/storage/upload-validation.d.ts +13 -0
  185. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
  186. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
  187. package/dist/strings.es-BeZWAodn.js +256 -0
  188. package/dist/strings.es-BeZWAodn.js.map +1 -0
  189. package/dist/upload-validation-Dd-fnsbA.js +172 -0
  190. package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
  191. package/dist/utils/byte-size.d.ts +10 -0
  192. package/dist/utils/compression.d.ts +6 -0
  193. package/dist/utils/load-failure.d.ts +14 -0
  194. package/package.json +5 -5
  195. package/dist/GCSStorageController-BSiP1c-f.js +0 -256
  196. package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
  197. package/dist/S3StorageController-CAwFRgjV.js +0 -276
  198. package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
  199. package/dist/auth-DMLngxn_.js.map +0 -1
  200. package/dist/backup-CN0s50D2.js.map +0 -1
  201. package/dist/contract-routes-fz8i4pxs.js.map +0 -1
  202. package/dist/cron-loader-CwaANlOG.js +0 -118
  203. package/dist/cron-loader-CwaANlOG.js.map +0 -1
  204. package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
  205. package/dist/cron-scheduler-CYQgco86.js.map +0 -1
  206. package/dist/cron-store-D2Q9-Aco.js +0 -300
  207. package/dist/cron-store-D2Q9-Aco.js.map +0 -1
  208. package/dist/dynamic-import-X40pTZUQ.js +0 -23
  209. package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
  210. package/dist/function-loader-D7o5Epjj.js.map +0 -1
  211. package/dist/function-routes-CaNG4waN.js.map +0 -1
  212. package/dist/jobs-DqYNfquG.js.map +0 -1
  213. package/dist/jwt-R6bSPMjk.js.map +0 -1
  214. package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
  215. package/dist/proxy-qRlqeUmO.js.map +0 -1
  216. package/dist/query-parser-BgiKJKvc.js.map +0 -1
  217. package/dist/request-timeout-DgH7j8qO.js.map +0 -1
  218. package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
  219. package/dist/soft-delete-params-BWPilMPF.js +0 -59
  220. package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
  221. package/dist/src-I3aG1PcY.js +0 -6870
  222. package/dist/src-I3aG1PcY.js.map +0 -1
  223. package/dist/types-BfKcm9do.js +0 -39
  224. package/dist/types-BfKcm9do.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract-routes-Ey6ftnKH.js","names":[],"sources":["../../types/src/types/collection_contract.ts","../../types/src/types/schema_version.ts","../src/api/contract-routes.ts"],"sourcesContent":["import type { CollectionConfig } from \"./collections\";\n\n/**\n * Serializing collections so they survive a network hop.\n *\n * A collection definition is not plain data. Relations point at their target\n * with a *function* (`target: () => usersCollection`) so two collections can\n * reference each other without an import cycle, and collections also carry\n * callbacks, custom views and component references. `JSON.stringify` silently\n * drops every one of those, which matters because the SDK generator *calls*\n * `relation.target()` to decide whether a foreign key is a string or a number.\n * Serialize naively and remote SDK generation produces subtly wrong types\n * instead of failing — the worst possible outcome.\n *\n * So relation targets are resolved to a slug reference on the way out and\n * rebuilt into functions on the way in. Everything else that cannot cross a wire\n * is dropped deliberately: an SDK is generated from the *shape* of the data, and\n * server-side behaviour is neither useful to a client nor safe to publish.\n */\n\n/** Marker replacing a relation's `target` function in serialized form. */\nexport interface SerializedCollectionRef {\n __collectionRef: string;\n}\n\nexport function isSerializedCollectionRef(value: unknown): value is SerializedCollectionRef {\n return typeof value === \"object\"\n && value !== null\n && typeof (value as SerializedCollectionRef).__collectionRef === \"string\";\n}\n\n/** Depth limit for the walk — deep enough for real configs, finite for cyclic ones. */\nconst MAX_DEPTH = 64;\n\n/**\n * Resolve whatever a `target` thunk returns down to a collection.\n *\n * A target may be the collection, a module namespace (when the authoring file\n * used `import * as`), or a default-export wrapper. All three appear in real\n * projects, and the SDK generator already unwraps them the same way.\n */\nfunction unwrapTarget(value: unknown): CollectionConfig | undefined {\n if (!value || typeof value !== \"object\") return undefined;\n const candidate = value as { default?: unknown; __esModule?: boolean; properties?: unknown };\n if (candidate.default || candidate.__esModule) {\n const inner = candidate.default;\n if (inner && typeof inner === \"object\") return inner as CollectionConfig;\n }\n if (candidate.properties) return value as CollectionConfig;\n return undefined;\n}\n\n/** The identity a serialized reference uses. Slug first — it is the routing key. */\nfunction refFor(collection: CollectionConfig | undefined): string | undefined {\n if (!collection) return undefined;\n const withPath = collection as CollectionConfig & { path?: string };\n return collection.slug || withPath.path || collection.name;\n}\n\n/**\n * Deep-copy a value into something JSON can carry.\n *\n * `target` keys are special-cased into refs. Other functions vanish, cycles are\n * cut, and everything else is copied structurally.\n */\n/** Shared walk state: the memo, plus a count of depth-cap hits. */\ninterface WalkState {\n memo: WeakMap<object, unknown>;\n /**\n * How many times the walk has truncated a subtree — by hitting the depth\n * cap, or by cutting a cycle.\n *\n * Either kind of truncation makes a result valid only at the *position* it\n * was produced at, so caching it and serving it elsewhere silently drops\n * content that would have been included. Comparing this counter before and\n * after a node's children tells us whether its result is position-\n * independent and therefore safe to memoize.\n *\n * The cycle case is the subtle one: with `a.b = b` and `b.a = a`, serializing\n * `{ first: b, second: a }` visits `a` beneath `b` — where the cycle back to\n * `b` is cut — and would then reuse that truncated `a` for `second`, where\n * nothing needed cutting.\n */\n truncations: number;\n}\n\nfunction toSerializable(\n value: unknown,\n seen: WeakSet<object>,\n depth: number,\n state: WalkState,\n key?: string\n): unknown {\n if (depth > MAX_DEPTH) {\n state.truncations++;\n return undefined;\n }\n\n if (typeof value === \"function\") {\n // Only a relation target carries information a client needs. Calling it\n // is safe here — this runs on the server, where the target module is\n // already loaded — and a throwing target simply yields no reference,\n // which degrades the generated FK type rather than failing the request.\n if (key === \"target\") {\n try {\n const resolved = unwrapTarget((value as () => unknown)());\n const ref = refFor(resolved);\n return ref ? { __collectionRef: ref } : undefined;\n } catch {\n return undefined;\n }\n }\n return undefined;\n }\n\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (value instanceof Date) return value.toISOString();\n if (value instanceof RegExp) return value.source;\n\n if (seen.has(value as object)) {\n state.truncations++;\n return undefined;\n }\n\n // A shared (non-cyclic) subgraph is reachable by many paths, and `seen` is a\n // *path* set — released in the `finally` below so a node referenced twice in\n // different branches is emitted twice rather than dropped as a false cycle.\n // Without memoization that makes the walk exponential in depth: a diamond\n // graph 20 levels deep took ~400ms, and each further level doubled it. The\n // result is a plain data tree, so handing back the same converted object for\n // a repeat visit is indistinguishable after JSON.stringify.\n const cached = state.memo.get(value as object);\n if (cached !== undefined) return cached;\n\n seen.add(value as object);\n const truncationsBefore = state.truncations;\n const memoize = (result: unknown): unknown => {\n // Only cache a result that nothing was cut from.\n if (result !== undefined && state.truncations === truncationsBefore) {\n state.memo.set(value as object, result);\n }\n return result;\n };\n\n try {\n if (Array.isArray(value)) {\n const items = value\n .map(item => toSerializable(item, seen, depth + 1, state))\n .filter(item => item !== undefined);\n // A container that had content, none of which can be represented, is\n // itself unrepresentable — see the note below.\n return memoize(value.length > 0 && items.length === 0 ? undefined : items);\n }\n\n // A React element or component reference has no meaning to a client and\n // will not survive JSON anyway.\n if (\"$$typeof\" in (value as Record<string, unknown>)) return undefined;\n\n const entries = Object.entries(value as Record<string, unknown>);\n const out: Record<string, unknown> = {};\n for (const [k, v] of entries) {\n const converted = toSerializable(v, seen, depth + 1, state, k);\n if (converted !== undefined) out[k] = converted;\n }\n\n // Drop a container whose entire content was dropped.\n //\n // `callbacks: { beforeSave() {…} }` would otherwise serialize to\n // `callbacks: {}` — an empty husk that carries no information but is not\n // *nothing*, so it lands in the payload and, worse, in the schema hash.\n // Editing a hook would then change every client's schema version and\n // report perfectly current SDKs as stale.\n //\n // A container that started empty stays empty: `properties: {}` is a\n // deliberate statement, not a casualty.\n if (entries.length > 0 && Object.keys(out).length === 0) return undefined;\n\n return memoize(out);\n } finally {\n // Released so a collection referenced twice in different branches is\n // emitted twice rather than being dropped as a false cycle.\n seen.delete(value as object);\n }\n}\n\n/**\n * Serialize collections for transport over the contract endpoint.\n *\n * Sorted by slug so the output — and therefore the schema hash computed from it\n * — does not depend on filesystem ordering.\n */\nexport function serializeCollections(collections: CollectionConfig[]): unknown[] {\n return [...collections]\n .sort((a, b) => String(a.slug ?? \"\").localeCompare(String(b.slug ?? \"\")))\n .map(collection => toSerializable(withoutAdminBlock(collection), new WeakSet(), 0, {\n memo: new WeakMap(),\n truncations: 0\n }))\n .filter((c): c is Record<string, unknown> => c !== undefined);\n}\n\n/**\n * Drop the admin block before the walk.\n *\n * Nothing downstream of serialization is an admin panel. The contract endpoint\n * feeds remote SDK generation, and `rebase build` writes the result into a bundle\n * manifest that only the backend runtime reads. The block would survive the walk\n * as a husk anyway — its React elements and component functions are dropped\n * individually — and that husk has two costs worth avoiding: it puts every custom\n * component's *file path* on an endpoint whose job is to describe data shapes, and\n * it grows a payload that is fetched and cached per project.\n *\n * Removing it here rather than at each call site means one chokepoint, so a future\n * consumer of `serializeCollections` cannot forget.\n *\n * Child collections carry their own block, so this recurses — stripping only the\n * top level was the mistake `stripNonClientFields` in the contract routes already\n * had to fix once for security rules.\n */\nfunction withoutAdminBlock(collection: CollectionConfig): CollectionConfig {\n const { admin: _admin, ...rest } = collection as CollectionConfig & Record<string, unknown>;\n const nested = rest as Record<string, unknown>;\n if (Array.isArray(nested.subcollections)) {\n nested.subcollections = nested.subcollections.map(\n (child) => withoutAdminBlock(child as CollectionConfig)\n );\n }\n return rest as CollectionConfig;\n}\n\n/**\n * Rebuild collections received from a contract endpoint.\n *\n * Relation refs become real thunks resolving through the returned set, so\n * downstream consumers — the SDK generator above all — see exactly the shape\n * they would have seen had the collections been imported from source.\n *\n * A ref naming a collection that is not in the payload resolves to `undefined`\n * rather than throwing: the generator already tolerates an unresolvable target\n * by falling back to a permissive key type, and a partial contract should still\n * produce a usable SDK.\n */\nexport function deserializeCollections(payload: unknown[]): CollectionConfig[] {\n const collections = payload\n .filter((c): c is Record<string, unknown> => typeof c === \"object\" && c !== null)\n .map(c => ({ ...c })) as unknown as CollectionConfig[];\n\n const bySlug = new Map<string, CollectionConfig>();\n for (const collection of collections) {\n const ref = refFor(collection);\n if (ref) bySlug.set(ref, collection);\n }\n\n const rehydrate = (value: unknown, depth: number): void => {\n if (depth > MAX_DEPTH || !value || typeof value !== \"object\") return;\n\n if (Array.isArray(value)) {\n for (const item of value) rehydrate(item, depth + 1);\n return;\n }\n\n const record = value as Record<string, unknown>;\n for (const [key, child] of Object.entries(record)) {\n if (key === \"target\" && isSerializedCollectionRef(child)) {\n const slug = child.__collectionRef;\n record.target = () => bySlug.get(slug);\n continue;\n }\n rehydrate(child, depth + 1);\n }\n };\n\n for (const collection of collections) rehydrate(collection, 0);\n return collections;\n}\n","import type { CollectionConfig } from \"./collections\";\nimport { serializeCollections } from \"./collection_contract\";\n\n/**\n * The schema version stamp.\n *\n * One function, used in three places that must agree or the whole drift-detection\n * story is noise: `rebase build` writes it into a bundle manifest, the runtime\n * serves it from the contract endpoint, and a generated SDK records the value it\n * was built from. If any two of those computed it differently, every client would\n * look permanently out of date.\n *\n * It covers **collections only** — the client's contract is the shape of the\n * data, so editing a hook or a server function must not invalidate every SDK in\n * every repository. That is a deliberate narrowing, not an oversight.\n */\n\n/** Stable stringify: object keys sorted at every level, so key order cannot alter the hash. */\nfunction canonicalize(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value) ?? \"null\";\n }\n if (Array.isArray(value)) {\n return `[${value.map(canonicalize).join(\",\")}]`;\n }\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`).join(\",\")}}`;\n}\n\n/**\n * Reduce a collection to the parts a generated client is actually built from.\n *\n * The version answers one question — \"is this SDK stale?\" — so it must change\n * exactly when the generated types could change, and never otherwise. Hashing a\n * whole collection fails both halves of that:\n *\n * - Security rules, callbacks, icons, groups and UI settings do not appear in a\n * generated client, so including them reports perfectly current SDKs as stale.\n * - Worse, they are not stable *inputs*. The runtime applies default security\n * rules when it loads collections, so the same source hashed before and after\n * loading produced two different answers — a build-time stamp that could never\n * match the server that served it.\n *\n * Codegen reads the slug (for the `Database` key and type names), the properties,\n * and the relations. That is the projection.\n */\nfunction projectForCodegen(collection: CollectionConfig): Record<string, unknown> {\n const source = collection as CollectionConfig & {\n relations?: unknown;\n subcollections?: CollectionConfig[];\n path?: string;\n engine?: unknown;\n dataSource?: unknown;\n };\n\n return {\n slug: collection.slug ?? source.path,\n properties: collection.properties,\n relations: source.relations,\n // The engine decides whether relations are resolved at all: codegen asks\n // `getDataSourceCapabilities(collection.engine).supportsRelations`, and an\n // engine that answers no drops every foreign-key column from the\n // generated Row/Insert/Update types. Moving a collection to such an\n // engine is a real change to the generated types, so it has to move the\n // version. `dataSource` is what resolves to `engine`, so it counts too.\n engine: source.engine,\n dataSource: source.dataSource,\n subcollections: source.subcollections?.map(projectForCodegen)\n };\n}\n\n/**\n * Compute the canonical string a schema version hashes.\n *\n * Exposed separately so the hashing itself can differ by environment: Node has\n * `crypto`, and callers without it can still compare canonical forms directly.\n */\nexport function canonicalSchemaPayload(collections: CollectionConfig[]): string {\n const projected = serializeCollections(collections)\n .map(collection => projectForCodegen(collection as CollectionConfig));\n return canonicalize(projected);\n}\n\n/**\n * A short, non-cryptographic digest of the canonical payload.\n *\n * FNV-1a style, 64 bits, as two 32-bit halves. This is an identity, not a\n * security boundary: nothing trusts a schema version to prove anything, it only\n * answers \"is this the same schema as before\". A hand-rolled hash keeps this\n * module free of `node:crypto`, so the identical function runs in the browser,\n * in the CLI, and in the runtime — which is the property that actually matters.\n */\nexport function computeSchemaVersion(collections: CollectionConfig[]): string {\n const payload = canonicalSchemaPayload(collections);\n\n let h1 = 0x811c9dc5;\n let h2 = 0x01000193;\n\n for (let i = 0; i < payload.length; i++) {\n const code = payload.charCodeAt(i);\n h1 ^= code;\n // Multiply by the FNV prime using shifts to stay in 32-bit integer math.\n h1 = (h1 + ((h1 << 1) + (h1 << 4) + (h1 << 7) + (h1 << 8) + (h1 << 24))) >>> 0;\n h2 ^= code + i;\n h2 = (h2 + ((h2 << 1) + (h2 << 5) + (h2 << 9) + (h2 << 15) + (h2 << 24))) >>> 0;\n }\n\n const hex = (n: number): string => n.toString(16).padStart(8, \"0\");\n return `v1:${hex(h1)}${hex(h2)}`;\n}\n","import { Hono } from \"hono\";\nimport {\n RUNTIME_CONTRACT_VERSION,\n SCHEMA_VERSION_HEADER,\n computeSchemaVersion,\n serializeCollections,\n type CollectionConfig,\n type RebaseProjectContract\n} from \"@rebasepro/types\";\nimport type { HonoEnv } from \"./types\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * The project contract endpoint.\n *\n * This is what makes a repository able to build against a project it does not\n * contain. Without it, a typed client can only be generated from local\n * collection *source*, which means every frontend must live in the same\n * repository as the backend. Serving the contract turns that around: an app\n * asks the project what its shape is, so a web app, a second web app and a\n * mobile app can each live wherever they like and none of them needs to know\n * about the others.\n *\n * Admin-gated. Collection definitions describe every table, column and relation\n * in the project, including ones no security rule would ever expose — that is a\n * map of the database, not public API documentation.\n */\n\nexport interface ContractRoutesConfig {\n collectionRegistry: { getRawCollections(): CollectionConfig[] };\n /**\n * The schema version recorded at build time.\n *\n * Preferred over recomputing, so that what a client is told matches exactly\n * what the bundle claims. It is recomputed only when a bundle did not record\n * one — a `baas`-mode project derives its collections from the live database\n * at boot, so there was nothing to hash when it was built.\n */\n schemaVersion?: string;\n /** Runtime package version, surfaced so a client can report what it built against. */\n runtimeVersion?: string;\n}\n\n/**\n * Strip everything a client does not need from a serialized collection.\n *\n * The generator reads the slug, the properties and the relations. It never reads\n * a security rule — but `securityRules` carries the raw SQL of every RLS\n * predicate guarding the project, which is a description of the authorization\n * model rather than of the data shape. Publishing it to anyone who can generate\n * an SDK gives away more than the endpoint is for, so it is removed here rather\n * than trusted not to matter.\n */\nfunction stripNonClientFields(collection: unknown): unknown {\n if (!collection || typeof collection !== \"object\") return collection;\n const {\n securityRules: _securityRules,\n callbacks: _callbacks,\n ...rest\n } = collection as Record<string, unknown>;\n\n // Subcollections are collections, and carry their own rules. Stripping only\n // the top level published every nested policy — the leak this exists to\n // prevent, just one level down.\n if (Array.isArray(rest.subcollections)) {\n rest.subcollections = rest.subcollections.map(stripNonClientFields);\n }\n\n return rest;\n}\n\nexport function createContractRoutes(config: ContractRoutesConfig): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n\n // Computing a version walks and canonicalizes every collection, and\n // `/schema-version` is deliberately unauthenticated and meant to be polled.\n // Recomputing per request would make a CI convenience into a CPU\n // amplification anyone could aim at the server. Collections do not change\n // after boot, so once is enough.\n let cachedVersion: string | undefined;\n const schemaVersionOf = (collections: CollectionConfig[]): string => {\n if (config.schemaVersion) return config.schemaVersion;\n if (cachedVersion === undefined) cachedVersion = computeSchemaVersion(collections);\n return cachedVersion;\n };\n\n router.get(\"/contract\", (c) => {\n const collections = config.collectionRegistry.getRawCollections();\n const serialized = serializeCollections(collections).map(stripNonClientFields);\n\n // In `baas` mode the collections are whatever introspection found at\n // boot, so the version has to be computed from them rather than taken\n // from a build that never saw them.\n const schemaVersion = schemaVersionOf(collections);\n\n const contract: RebaseProjectContract = {\n schemaVersion,\n runtime: {\n version: config.runtimeVersion ?? \"unknown\",\n contract: RUNTIME_CONTRACT_VERSION\n },\n collections: serialized,\n collectionSlugs: collections\n .map(collection => collection.slug)\n .filter((slug): slug is string => Boolean(slug))\n .sort(),\n generatedAt: new Date().toISOString()\n };\n\n c.header(SCHEMA_VERSION_HEADER, schemaVersion);\n return c.json(contract);\n });\n\n /**\n * Cheap drift check.\n *\n * Deliberately unauthenticated and deliberately tiny: two version stamps\n * and nothing else. A CI job that only wants to know whether its generated\n * SDK is stale should not need admin credentials, and a version stamp\n * reveals nothing about the schema it stands for.\n *\n * `runtime` is here as well as on `/contract` because the two answer\n * different questions and only one of them was reachable. Which runtime a\n * project is on decides whether a client's wire format is understood at\n * all, and it was published solely on the admin-gated route — so a CLI or\n * an SDK, the two callers that actually need to know, could not ask. That\n * is the same shape as the header this route echoes: a documented signal\n * with no reachable sender. `contract` is the number that matters for\n * compatibility; `version` names the release a human should quote.\n */\n router.get(\"/schema-version\", (c) => {\n const schemaVersion = schemaVersionOf(config.collectionRegistry.getRawCollections());\n c.header(SCHEMA_VERSION_HEADER, schemaVersion);\n return c.json({\n schemaVersion,\n runtime: {\n version: config.runtimeVersion ?? \"unknown\",\n contract: RUNTIME_CONTRACT_VERSION\n }\n });\n });\n\n logger.debug(\"Contract routes mounted\");\n return router;\n}\n"],"mappings":"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"}
@@ -1,5 +1,19 @@
1
1
  import { type CronJobDefinition, type CronResourceOptions } from "@rebasepro/types";
2
2
  import { type ModuleImporter } from "../utils/dynamic-import.js";
3
+ import type { LoadDirectoryOptions } from "../functions/function-loader.js";
4
+ /**
5
+ * Cron jobs as values: the job id, to its definition.
6
+ *
7
+ * `crons: { "nightly-report": nightlyReport }` schedules `nightlyReport` as the
8
+ * job `nightly-report` — the same thing a file named `crons/nightly-report.ts`
9
+ * does. {@link loadCrons} returns exactly this shape, so
10
+ * `crons: await loadCrons(dir)` and `cronsDir: dir` schedule the same jobs.
11
+ *
12
+ * A value is what the author wrote — `defineCron({...})` — with no defaults
13
+ * filled in. The scheduler fills them in when it registers the job.
14
+ */
15
+ export type CronsRecord = Record<string, CronJobDefinition>;
16
+ /** A job as the scheduler registers it: its id, and its definition with defaults filled in. */
3
17
  export interface LoadedCronJob {
4
18
  /** Job ID derived from filename (e.g. "cleanup-sessions"). */
5
19
  id: string;
@@ -13,34 +27,79 @@ export interface LoadedCronJobs {
13
27
  problems: string[];
14
28
  }
15
29
  /**
16
- * Auto-discover cron job files from a directory.
30
+ * Load the cron jobs in a directory, as values.
31
+ *
32
+ * Each file default-exports a `CronJobDefinition` — `defineCron({...})`. The
33
+ * filename without its extension is the record key, which is the job id:
34
+ * `crons/cleanup-sessions.ts` → `{ "cleanup-sessions": definition }`.
35
+ *
36
+ * Loading declares nothing: the definitions come back exactly as written, and
37
+ * whoever uses them decides what they are for — a backend schedules them, the
38
+ * build's derive step records them in the resource graph ({@link declareCrons}).
39
+ *
40
+ * Never throws for a bad file: it is skipped, logged, and passed to
41
+ * `options.onSkip`.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const crons = await loadCrons(path.resolve("crons"));
46
+ * const backend = await createRebaseBackend({ database, crons, provision: true });
47
+ * ```
48
+ */
49
+ export declare function loadCrons(directory: string, options?: LoadDirectoryOptions): Promise<CronsRecord>;
50
+ /**
51
+ * Record each cron in the project's resource graph — what `rebase resources`
52
+ * lists and a host reads to know a schedule exists, without running it.
17
53
  *
18
- * Each file should default-export a `CronJobDefinition`.
19
- * The filename (without extension) becomes the job ID:
20
- * `crons/cleanup-sessions.ts` → id = "cleanup-sessions"
54
+ * Keyed by the same id the scheduler, the routes and the Studio use, and built
55
+ * from the definitions as written: a default the scheduler fills in
56
+ * (`enabled: true`, `timeoutSeconds: 300`) is this process's, not something the
57
+ * author declared.
58
+ */
59
+ export declare function declareCrons(crons: CronsRecord): void;
60
+ /**
61
+ * The jobs a scheduler registers, from cron values: the definitions with this
62
+ * process's defaults filled in.
63
+ *
64
+ * Spread first, then normalise. Rebuilding the object field by field dropped
65
+ * `catchUpWindowSeconds` once — and since this is the path every job takes to
66
+ * the scheduler, catch-up never ran for any job authored the documented way. A
67
+ * field added to `CronJobDefinition` tomorrow would be dropped the same way.
21
68
  *
22
- * Follows the same discovery pattern as `loadFunctionsFromDirectory` — see
23
- * {@link loadCronJobsWithDiagnostics} for what happens to the files it cannot
24
- * load.
69
+ * @throws when a value has no `schedule` or no `handler`. A file like that is
70
+ * skipped by the loader with a reason; a value handed over in code is refused,
71
+ * because the caller is right there to fix it.
72
+ */
73
+ export declare function cronJobsFrom(crons: CronsRecord): LoadedCronJob[];
74
+ /**
75
+ * @deprecated Use `loadCrons` instead, which returns the jobs as a record keyed
76
+ * by id. Will be removed in a future release.
25
77
  */
26
78
  export declare function loadCronJobsFromDirectory(directory: string, importModule?: ModuleImporter): Promise<LoadedCronJob[]>;
27
79
  /**
28
- * {@link loadCronJobsFromDirectory}, plus the list of files that were skipped.
80
+ * @deprecated Use `loadCrons` instead, with `onSkip` to collect the files it
81
+ * skipped — and `declareCrons` where the jobs belong in the resource graph,
82
+ * which this used to do as a side effect of loading. Will be removed in a
83
+ * future release.
84
+ */
85
+ export declare function loadCronJobsWithDiagnostics(directory: string, importModule?: ModuleImporter): Promise<LoadedCronJobs>;
86
+ /**
87
+ * Read a directory of cron files: the definitions that loaded, keyed by id, and
88
+ * one `<file> (<reason>)` per file that will NOT be scheduled.
29
89
  *
30
- * A job that does not load does not run, and until now the only trace was one
90
+ * A job that does not load does not run, and the only trace used to be one
31
91
  * `warn` per file in the boot log — next to nothing at 3am when the report that
32
- * should have been mailed was not. A cron file is written once and then trusted
33
- * for months, so "it silently never ran" is the failure mode this surface has,
34
- * and a log line is not a surface.
92
+ * should have been mailed was not. So the skips are counted, summarised in one
93
+ * place, and answered by `GET /api/admin/cron`.
35
94
  *
36
- * Never throws, for the same reason `loadFunctionsWithDiagnostics` does not: one
95
+ * Never throws, for the same reason the function loader does not: one
37
96
  * malformed file must not take the server down, and the other jobs in the
38
- * directory are still worth scheduling. The difference is that the skips are now
39
- * counted, summarised in one place, and answered by `GET /api/cron` — so the
40
- * question "why did nothing run?" has an answer that does not require log
41
- * access.
97
+ * directory are still worth scheduling.
42
98
  */
43
- export declare function loadCronJobsWithDiagnostics(directory: string, importModule?: ModuleImporter): Promise<LoadedCronJobs>;
99
+ export declare function readCronsDirectory(directory: string, importModule?: ModuleImporter): Promise<{
100
+ crons: CronsRecord;
101
+ problems: string[];
102
+ }>;
44
103
  /**
45
104
  * The declaration a definition makes: every field a host can read without the
46
105
  * handler. Spelled once here so the graph and the scheduler cannot disagree
@@ -7,7 +7,7 @@ import { type CronScheduler } from "./cron-scheduler.js";
7
7
  * Routes:
8
8
  * GET / → list all cron jobs
9
9
  * GET /:id → get a single job's status
10
- * POST /:id/trigger → manually trigger a job
10
+ * POST /:id/trigger → manually trigger a job (`?async=1`: 202 once it has started)
11
11
  * GET /:id/logs → get execution logs for a job
12
12
  * PUT /:id → update job (enable/disable, or `null` to follow the code)
13
13
  *
@@ -30,9 +30,9 @@ export declare function parseCronExpression(expression: string, after: Date, tim
30
30
  * The latest slot matching `expression` within the inclusive window
31
31
  * `[from, to]`, or `undefined` when the expression has no slot in it.
32
32
  *
33
- * Walks backwards a minute at a time from `to`, so the first hit is already
34
- * the answer — in the common case (a job that ran normally moments ago) that
35
- * is a handful of iterations, not a scan of the whole window.
33
+ * Walks backwards from `to`, so the first hit is already the answer — in the
34
+ * common case (a job that ran normally moments ago) that is a handful of
35
+ * steps, not a scan of the whole window.
36
36
  *
37
37
  * `to`'s own minute is included: an instance booting at 06:00:30 has *not* run
38
38
  * the 06:00 slot — `parseCronExpression` already skipped past it to tomorrow —
@@ -43,6 +43,29 @@ export declare function parseCronExpression(expression: string, after: Date, tim
43
43
  * ISO string down either path. The claim key depends on that.
44
44
  */
45
45
  export declare function findMostRecentSlot(expression: string, from: Date, to: Date, timezone?: string): Date | undefined;
46
+ /**
47
+ * What one run keeps of its `ctx.log` lines: up to 500 lines or 64 KB,
48
+ * whichever comes first. The lines past that are counted, and one line at the
49
+ * end says how many there were; every line still reaches the server log.
50
+ *
51
+ * Uncapped, a job logging one line per item kept them all — in the 50-run ring
52
+ * per job, in a `cron_logs` row, and in every history read: a per-minute sync
53
+ * of 1,000 items held 4 MB per job in memory and wrote 115 MB a day.
54
+ */
55
+ export declare const MAX_CAPTURED_LOG_LINES = 500;
56
+ export declare const MAX_CAPTURED_LOG_BYTES: number;
57
+ /**
58
+ * What {@link CronScheduler.startJob} answers: the run began at `startedAt`
59
+ * (its history entry will carry the same timestamp), or it did not start and
60
+ * `log` is the skip that says why.
61
+ */
62
+ export type StartedCronRun = {
63
+ started: true;
64
+ startedAt: string;
65
+ } | {
66
+ started: false;
67
+ log: CronJobLogEntry;
68
+ };
46
69
  /**
47
70
  * Whether a log entry records a run that did not happen because one was
48
71
  * already executing — on this process, or on another that holds the job's run
@@ -69,6 +92,10 @@ export declare class CronScheduler {
69
92
  * ever clear the lease that run took.
70
93
  */
71
94
  private readonly instanceLabel;
95
+ /** The hourly claim- and log-retention sweep, while the scheduler is started with a store. */
96
+ private claimSweepTimer?;
97
+ private sweepingClaims;
98
+ private sweepingLogs;
72
99
  /**
73
100
  * Set the server singleton to make it available to cron job handlers.
74
101
  *
@@ -98,6 +125,14 @@ export declare class CronScheduler {
98
125
  * Start the scheduler — begins ticking all enabled jobs.
99
126
  */
100
127
  start(): void;
128
+ /** Delete slot claims past retention. Failures are logged; the next hour tries again. */
129
+ private sweepClaims;
130
+ /**
131
+ * Delete run history past retention, and count the runs not counted yet.
132
+ * Failures are logged; the next hour tries again, from where this one
133
+ * stopped — each batch it finished is committed.
134
+ */
135
+ private sweepLogs;
101
136
  /**
102
137
  * Stop the scheduler: clear every timer, then wait for the runs executing
103
138
  * right now — for at most `timeoutMs` when given.
@@ -204,6 +239,22 @@ export declare class CronScheduler {
204
239
  * recognises it.
205
240
  */
206
241
  triggerJob(id: string): Promise<CronJobLogEntry | undefined>;
242
+ /**
243
+ * Start a job now and answer as soon as the run has begun, not when it
244
+ * ends — the manual trigger's `?async=1`.
245
+ *
246
+ * The synchronous {@link triggerJob} holds its HTTP request open for the
247
+ * whole run, and a proxy in front of the server (many stop at 60 s) cuts
248
+ * a long job's answer while the run carries on. This resolves once the
249
+ * run lease is held and the handler is about to be called, so a run that
250
+ * cannot start — already executing here or on the lease holder — is still
251
+ * answered as the skip it is. The run itself is tracked like any other:
252
+ * `stop()` waits for it, and its outcome lands in the run history, where
253
+ * the entry carries `manual: true` and the `startedAt` returned here.
254
+ *
255
+ * Returns `undefined` if the job doesn't exist.
256
+ */
257
+ startJob(id: string): Promise<StartedCronRun | undefined>;
207
258
  /**
208
259
  * Record a run that did not happen because the previous one had not
209
260
  * finished.
@@ -267,6 +318,21 @@ export declare class CronScheduler {
267
318
  * rest as a view for {@link toStatus}. Never throws.
268
319
  */
269
320
  private readFleet;
321
+ /**
322
+ * The next slot of `job` after `now`, for a listing on a process whose
323
+ * scheduler is not started — the `api` role, which Studio's Cron panel
324
+ * polls every 15 seconds.
325
+ *
326
+ * Cached on the job. The key is the job's schedule and zone (a re-register
327
+ * replaces the job, and with it the cache); the entry holds while
328
+ * `computedAt <= now < slot`. That is the same answer, not an
329
+ * approximation: the search found no matching minute between
330
+ * `computedAt`'s next minute and `slot`, so from any later instant before
331
+ * `slot` it lands on `slot` again. A clock that moves back, or reaching the
332
+ * slot, recomputes. The cache holds a pure function of the schedule, so
333
+ * there is nothing user- or role-scoped in it.
334
+ */
335
+ private listedNextRun;
270
336
  /**
271
337
  * Warn once at start when the process looks like it is running on a
272
338
  * platform that freezes or evicts instances between requests, where the
@@ -11,7 +11,12 @@ import type { DataDriver } from "@rebasepro/types";
11
11
  * occurs, and a pause or a trigger's "already executing" is per process.
12
12
  */
13
13
  export interface CronStore {
14
- /** Ensure the backing table exists. Called once on startup. */
14
+ /**
15
+ * Ensure the backing tables exist and are locked away from end users. Runs
16
+ * once per process per database, however many stores ask: concurrent
17
+ * callers share the attempt in flight, and an attempt that fell short is
18
+ * made again on the next call.
19
+ */
15
20
  ensureTable(): Promise<void>;
16
21
  /** Persist a single log entry after execution. */
17
22
  insertLog(entry: CronJobLogEntry): Promise<void>;
@@ -28,7 +33,8 @@ export interface CronStore {
28
33
  fetchLogs(jobId: string, limit?: number): Promise<CronJobLogEntry[]>;
29
34
  /**
30
35
  * Fetch aggregate stats for all jobs (totalRuns, totalFailures, lastRunAt).
31
- * Used to seed in-memory counters on startup.
36
+ * Used to seed in-memory counters on startup. The SQL store reads them from
37
+ * the all-time counters on `cron_job_state`, not from the history.
32
38
  */
33
39
  fetchJobStats(): Promise<Map<string, {
34
40
  totalRuns: number;
@@ -86,11 +92,33 @@ export interface CronStore {
86
92
  /** Release a lease taken with {@link tryAcquireRunLease} — only if `holder` still holds it. */
87
93
  releaseRunLease?(jobId: string, holder: string): Promise<void>;
88
94
  /**
89
- * Run count, failure count and last run of each job, read from
90
- * `cron_logs` — every process's runs, where the scheduler's own counters
91
- * hold only the ones it made.
95
+ * Run count, failure count and last run of each job — every process's
96
+ * runs, where the scheduler's own counters hold only the ones it made. The
97
+ * SQL store keeps them as all-time counters on `cron_job_state`, so they
98
+ * outlive the history's retention.
92
99
  */
93
100
  fetchRunSummaries?(jobIds: readonly string[]): Promise<Map<string, CronJobRunSummary>>;
101
+ /**
102
+ * Delete slot claims past retention — all but each job's most recent one —
103
+ * in batches. Resolves with how many went. The scheduler runs it on a
104
+ * timer while it is started; a store without it keeps every claim.
105
+ */
106
+ sweepClaims?(): Promise<number>;
107
+ /**
108
+ * Delete run history past retention, and count into the job counters any
109
+ * run that is not counted yet — a history written before the counters
110
+ * existed, or by an older server during a rolling deploy. Both in batches.
111
+ * The scheduler runs it with {@link sweepClaims}; a store without it keeps
112
+ * every run.
113
+ */
114
+ sweepLogs?(): Promise<CronLogSweep>;
115
+ }
116
+ /** What one {@link CronStore.sweepLogs} did. */
117
+ export interface CronLogSweep {
118
+ /** History rows deleted past retention. */
119
+ deleted: number;
120
+ /** Runs counted into the job counters that were not counted before. */
121
+ counted: number;
94
122
  }
95
123
  /** One job's row in `rebase.cron_job_state`, as the scheduler reads it. */
96
124
  export interface CronJobPersistedState {
@@ -1,4 +1,4 @@
1
- import type { CronJobDefinition } from "@rebasepro/types";
1
+ import type { CronJobDefinition, RegisteredDatabase } from "@rebasepro/types";
2
2
  /**
3
3
  * Typed authoring helper for a cron job file. Identity at runtime —
4
4
  * a plain default-exported {@link CronJobDefinition} works identically;
@@ -38,5 +38,9 @@ import type { CronJobDefinition } from "@rebasepro/types";
38
38
  * },
39
39
  * });
40
40
  * ```
41
+ *
42
+ * `ctx.rebase` is typed from the `Database` the project registered on
43
+ * `Register` (see the typed server data guide), and untyped when it registered
44
+ * none. `defineCron<Database>({…})` types one job without the registration.
41
45
  */
42
- export declare function defineCron(definition: CronJobDefinition): CronJobDefinition;
46
+ export declare function defineCron<DB = RegisteredDatabase>(definition: CronJobDefinition<DB>): CronJobDefinition<DB>;
@@ -1,8 +1,8 @@
1
- export { loadCronJobsFromDirectory, loadCronJobsWithDiagnostics } from "./cron-loader.js";
2
- export type { LoadedCronJob, LoadedCronJobs } from "./cron-loader.js";
1
+ export { loadCrons, declareCrons, cronJobsFrom, loadCronJobsFromDirectory, loadCronJobsWithDiagnostics } from "./cron-loader.js";
2
+ export type { CronsRecord, LoadedCronJob, LoadedCronJobs } from "./cron-loader.js";
3
3
  export { defineCron } from "./define-cron.js";
4
4
  export { CronScheduler, validateCronExpression } from "./cron-scheduler.js";
5
5
  export { createCronRoutes } from "./cron-routes.js";
6
6
  export { createCronStore } from "./cron-store.js";
7
7
  export { detectFreezableRuntime, buildScaleToZeroWarning, CRON_ALWAYS_ON_ENV } from "./scale-to-zero.js";
8
- export type { CronStore, CronJobPersistedState, CronRunLease, CronJobRunSummary } from "./cron-store.js";
8
+ export type { CronStore, CronJobPersistedState, CronRunLease, CronJobRunSummary, CronLogSweep } from "./cron-store.js";
@@ -0,0 +1,202 @@
1
+ import { createRequire as __rebaseCreateRequire } from "module";
2
+ import __rebaseProcess from "process";
3
+ globalThis.process ??= __rebaseProcess;
4
+ __rebaseCreateRequire(import.meta.url);
5
+ import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
+ import { x as declareCron } from "./src-D5Y3N-S4.js";
7
+ import { r as logger } from "./logger-D-S-hO5e.js";
8
+ import { i as warnDeprecated, n as describeLoadFailure, r as nativeDynamicImport, t as SYNTAX_ERROR_ADVICE } from "./load-failure-L5hvfDoG.js";
9
+ import * as fs$1 from "fs";
10
+ import * as path$1 from "path";
11
+ import { pathToFileURL } from "url";
12
+ //#region src/cron/cron-loader.ts
13
+ var cron_loader_exports = /* @__PURE__ */ __exportAll({
14
+ cronJobsFrom: () => cronJobsFrom,
15
+ cronResourceOptions: () => cronResourceOptions,
16
+ declareCrons: () => declareCrons,
17
+ loadCronJobsFromDirectory: () => loadCronJobsFromDirectory,
18
+ loadCronJobsWithDiagnostics: () => loadCronJobsWithDiagnostics,
19
+ loadCrons: () => loadCrons,
20
+ readCronsDirectory: () => readCronsDirectory
21
+ });
22
+ /**
23
+ * Load the cron jobs in a directory, as values.
24
+ *
25
+ * Each file default-exports a `CronJobDefinition` — `defineCron({...})`. The
26
+ * filename without its extension is the record key, which is the job id:
27
+ * `crons/cleanup-sessions.ts` → `{ "cleanup-sessions": definition }`.
28
+ *
29
+ * Loading declares nothing: the definitions come back exactly as written, and
30
+ * whoever uses them decides what they are for — a backend schedules them, the
31
+ * build's derive step records them in the resource graph ({@link declareCrons}).
32
+ *
33
+ * Never throws for a bad file: it is skipped, logged, and passed to
34
+ * `options.onSkip`.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const crons = await loadCrons(path.resolve("crons"));
39
+ * const backend = await createRebaseBackend({ database, crons, provision: true });
40
+ * ```
41
+ */
42
+ async function loadCrons(directory, options = {}) {
43
+ const { crons, problems } = await readCronsDirectory(directory, options.importModule);
44
+ if (options.onSkip) for (const problem of problems) options.onSkip(problem);
45
+ return crons;
46
+ }
47
+ /**
48
+ * Record each cron in the project's resource graph — what `rebase resources`
49
+ * lists and a host reads to know a schedule exists, without running it.
50
+ *
51
+ * Keyed by the same id the scheduler, the routes and the Studio use, and built
52
+ * from the definitions as written: a default the scheduler fills in
53
+ * (`enabled: true`, `timeoutSeconds: 300`) is this process's, not something the
54
+ * author declared.
55
+ */
56
+ function declareCrons(crons) {
57
+ for (const [id, definition] of Object.entries(crons)) declareCron(id, cronResourceOptions(definition));
58
+ }
59
+ /**
60
+ * The jobs a scheduler registers, from cron values: the definitions with this
61
+ * process's defaults filled in.
62
+ *
63
+ * Spread first, then normalise. Rebuilding the object field by field dropped
64
+ * `catchUpWindowSeconds` once — and since this is the path every job takes to
65
+ * the scheduler, catch-up never ran for any job authored the documented way. A
66
+ * field added to `CronJobDefinition` tomorrow would be dropped the same way.
67
+ *
68
+ * @throws when a value has no `schedule` or no `handler`. A file like that is
69
+ * skipped by the loader with a reason; a value handed over in code is refused,
70
+ * because the caller is right there to fix it.
71
+ */
72
+ function cronJobsFrom(crons) {
73
+ return Object.entries(crons).map(([id, definition]) => {
74
+ const def = definition;
75
+ if (!def || typeof def.schedule !== "string" || typeof def.handler !== "function") throw new Error(`crons["${id}"] is not a cron job: it needs a \`schedule\` string and a \`handler\` function. Author it with \`defineCron({ schedule, handler })\`.`);
76
+ return {
77
+ id,
78
+ definition: {
79
+ ...def,
80
+ schedule: def.schedule,
81
+ name: def.name || id,
82
+ enabled: def.enabled !== false,
83
+ timeoutSeconds: def.timeoutSeconds ?? 300,
84
+ handler: def.handler
85
+ }
86
+ };
87
+ });
88
+ }
89
+ /**
90
+ * @deprecated Use `loadCrons` instead, which returns the jobs as a record keyed
91
+ * by id. Will be removed in a future release.
92
+ */
93
+ async function loadCronJobsFromDirectory(directory, importModule = nativeDynamicImport) {
94
+ warnDeprecated("server.loadCronJobsFromDirectory", "`loadCronJobsFromDirectory` is deprecated; use `loadCrons(dir)`, which returns `{ [id]: definition }`.");
95
+ return (await legacyLoad(directory, importModule)).jobs;
96
+ }
97
+ /**
98
+ * @deprecated Use `loadCrons` instead, with `onSkip` to collect the files it
99
+ * skipped — and `declareCrons` where the jobs belong in the resource graph,
100
+ * which this used to do as a side effect of loading. Will be removed in a
101
+ * future release.
102
+ */
103
+ async function loadCronJobsWithDiagnostics(directory, importModule = nativeDynamicImport) {
104
+ warnDeprecated("server.loadCronJobsWithDiagnostics", "`loadCronJobsWithDiagnostics` is deprecated; use `loadCrons(dir, { onSkip })`, which returns `{ [id]: definition }`, and `declareCrons(crons)` to record them in the resource graph.");
105
+ return legacyLoad(directory, importModule);
106
+ }
107
+ /**
108
+ * What the deprecated loaders did: load, declare every job in the resource
109
+ * graph, and hand back normalised jobs. A job the graph refuses is skipped as a
110
+ * problem, as it was when the declaration ran inside the loader.
111
+ */
112
+ async function legacyLoad(directory, importModule) {
113
+ const { crons, problems } = await readCronsDirectory(directory, importModule);
114
+ const declared = {};
115
+ for (const [id, definition] of Object.entries(crons)) try {
116
+ declareCron(id, cronResourceOptions(definition));
117
+ declared[id] = definition;
118
+ } catch (err) {
119
+ problems.push(describeLoadFailure(id, err));
120
+ }
121
+ return {
122
+ jobs: cronJobsFrom(declared),
123
+ problems
124
+ };
125
+ }
126
+ /**
127
+ * Read a directory of cron files: the definitions that loaded, keyed by id, and
128
+ * one `<file> (<reason>)` per file that will NOT be scheduled.
129
+ *
130
+ * A job that does not load does not run, and the only trace used to be one
131
+ * `warn` per file in the boot log — next to nothing at 3am when the report that
132
+ * should have been mailed was not. So the skips are counted, summarised in one
133
+ * place, and answered by `GET /api/admin/cron`.
134
+ *
135
+ * Never throws, for the same reason the function loader does not: one
136
+ * malformed file must not take the server down, and the other jobs in the
137
+ * directory are still worth scheduling.
138
+ */
139
+ async function readCronsDirectory(directory, importModule = nativeDynamicImport) {
140
+ const crons = {};
141
+ const problems = [];
142
+ if (!fs$1.existsSync(directory)) return {
143
+ crons,
144
+ problems
145
+ };
146
+ const files = fs$1.readdirSync(directory);
147
+ for (const file of files) if ((file.endsWith(".ts") || file.endsWith(".js")) && !file.startsWith(".") && !file.includes(".test.") && !file.endsWith(".d.ts") && file !== "index.ts" && file !== "index.js") {
148
+ const filePath = path$1.join(directory, file);
149
+ try {
150
+ const fileUrl = pathToFileURL(filePath).href;
151
+ const exported = (await importModule(fileUrl)).default;
152
+ if (!exported || typeof exported !== "object") {
153
+ problems.push(`${file} (no default export)`);
154
+ logger.warn(`[cron] ${file}: no valid default export. Skipping.`);
155
+ continue;
156
+ }
157
+ const def = exported;
158
+ if (typeof def.schedule !== "string" || def.schedule.trim() === "" || typeof def.handler !== "function") {
159
+ problems.push(`${file} (default export has no 'schedule' or no 'handler')`);
160
+ logger.warn(`[cron] ${file}: default export missing required 'schedule' or 'handler'. Skipping.`);
161
+ continue;
162
+ }
163
+ const id = path$1.basename(file, path$1.extname(file));
164
+ const definition = exported;
165
+ crons[id] = definition;
166
+ logger.info(`⏰ Loaded cron job: ${id} (${definition.schedule}${definition.timezone ? ` ${definition.timezone}` : " in the process's local time"})`);
167
+ } catch (err) {
168
+ const message = err instanceof Error ? err.message : String(err);
169
+ problems.push(describeLoadFailure(file, err));
170
+ logger.debug(`[cron] Failed to load ${file}: ${message}`);
171
+ }
172
+ }
173
+ if (problems.length > 0) {
174
+ const syntax = problems.some((p) => p.includes("(syntax error"));
175
+ const other = problems.some((p) => !p.includes("(syntax error"));
176
+ logger.warn(`[cron] ${problems.length} cron file(s) were skipped and will NOT be scheduled:\n` + problems.map((p) => ` - ${p}`).join("\n") + "\n" + [...syntax ? [SYNTAX_ERROR_ADVICE] : [], ...other ? [" Fix these or author them with `defineCron(...)` for a typed, compile-checked contract."] : []].join("\n"));
177
+ }
178
+ return {
179
+ crons,
180
+ problems
181
+ };
182
+ }
183
+ /**
184
+ * The declaration a definition makes: every field a host can read without the
185
+ * handler. Spelled once here so the graph and the scheduler cannot disagree
186
+ * about what a cron is.
187
+ */
188
+ function cronResourceOptions(definition) {
189
+ return {
190
+ schedule: definition.schedule,
191
+ ...definition.timezone !== void 0 ? { timezone: definition.timezone } : {},
192
+ ...definition.description !== void 0 ? { description: definition.description } : {},
193
+ ...definition.enabled !== void 0 ? { enabled: definition.enabled } : {},
194
+ ...definition.timeoutSeconds !== void 0 ? { timeoutSeconds: definition.timeoutSeconds } : {},
195
+ ...definition.catchUpWindowSeconds !== void 0 ? { catchUpWindowSeconds: definition.catchUpWindowSeconds } : {},
196
+ ...definition.name ? { label: definition.name } : {}
197
+ };
198
+ }
199
+ //#endregion
200
+ export { loadCrons as a, loadCronJobsWithDiagnostics as i, declareCrons as n, loadCronJobsFromDirectory as r, cron_loader_exports as t };
201
+
202
+ //# sourceMappingURL=cron-loader-B-u0snqq.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron-loader-B-u0snqq.js","names":[],"sources":["../src/cron/cron-loader.ts"],"sourcesContent":["import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { declareCron, type CronJobDefinition, type CronResourceOptions } from \"@rebasepro/types\";\nimport { logger } from \"../utils/logger.js\";\nimport { nativeDynamicImport, type ModuleImporter } from \"../utils/dynamic-import.js\";\nimport { describeLoadFailure, SYNTAX_ERROR_ADVICE } from \"../utils/load-failure.js\";\nimport { warnDeprecated } from \"@rebasepro/common\";\nimport type { LoadDirectoryOptions } from \"../functions/function-loader.js\";\n\n/**\n * Cron jobs as values: the job id, to its definition.\n *\n * `crons: { \"nightly-report\": nightlyReport }` schedules `nightlyReport` as the\n * job `nightly-report` — the same thing a file named `crons/nightly-report.ts`\n * does. {@link loadCrons} returns exactly this shape, so\n * `crons: await loadCrons(dir)` and `cronsDir: dir` schedule the same jobs.\n *\n * A value is what the author wrote — `defineCron({...})` — with no defaults\n * filled in. The scheduler fills them in when it registers the job.\n */\nexport type CronsRecord = Record<string, CronJobDefinition>;\n\n/** A job as the scheduler registers it: its id, and its definition with defaults filled in. */\nexport interface LoadedCronJob {\n /** Job ID derived from filename (e.g. \"cleanup-sessions\"). */\n id: string;\n /** The full definition. */\n definition: CronJobDefinition;\n}\n\n/** {@link loadCronJobsWithDiagnostics}' answer: what loaded, and what did not. */\nexport interface LoadedCronJobs {\n jobs: LoadedCronJob[];\n /** One entry per file that will NOT be scheduled, with the reason. */\n problems: string[];\n}\n\n/**\n * Load the cron jobs in a directory, as values.\n *\n * Each file default-exports a `CronJobDefinition` — `defineCron({...})`. The\n * filename without its extension is the record key, which is the job id:\n * `crons/cleanup-sessions.ts` → `{ \"cleanup-sessions\": definition }`.\n *\n * Loading declares nothing: the definitions come back exactly as written, and\n * whoever uses them decides what they are for — a backend schedules them, the\n * build's derive step records them in the resource graph ({@link declareCrons}).\n *\n * Never throws for a bad file: it is skipped, logged, and passed to\n * `options.onSkip`.\n *\n * @example\n * ```ts\n * const crons = await loadCrons(path.resolve(\"crons\"));\n * const backend = await createRebaseBackend({ database, crons, provision: true });\n * ```\n */\nexport async function loadCrons(directory: string, options: LoadDirectoryOptions = {}): Promise<CronsRecord> {\n const { crons, problems } = await readCronsDirectory(directory, options.importModule);\n if (options.onSkip) for (const problem of problems) options.onSkip(problem);\n return crons;\n}\n\n/**\n * Record each cron in the project's resource graph — what `rebase resources`\n * lists and a host reads to know a schedule exists, without running it.\n *\n * Keyed by the same id the scheduler, the routes and the Studio use, and built\n * from the definitions as written: a default the scheduler fills in\n * (`enabled: true`, `timeoutSeconds: 300`) is this process's, not something the\n * author declared.\n */\nexport function declareCrons(crons: CronsRecord): void {\n for (const [id, definition] of Object.entries(crons)) {\n declareCron(id, cronResourceOptions(definition));\n }\n}\n\n/**\n * The jobs a scheduler registers, from cron values: the definitions with this\n * process's defaults filled in.\n *\n * Spread first, then normalise. Rebuilding the object field by field dropped\n * `catchUpWindowSeconds` once — and since this is the path every job takes to\n * the scheduler, catch-up never ran for any job authored the documented way. A\n * field added to `CronJobDefinition` tomorrow would be dropped the same way.\n *\n * @throws when a value has no `schedule` or no `handler`. A file like that is\n * skipped by the loader with a reason; a value handed over in code is refused,\n * because the caller is right there to fix it.\n */\nexport function cronJobsFrom(crons: CronsRecord): LoadedCronJob[] {\n return Object.entries(crons).map(([id, definition]) => {\n const def = definition as Partial<CronJobDefinition> | undefined;\n if (!def || typeof def.schedule !== \"string\" || typeof def.handler !== \"function\") {\n throw new Error(\n `crons[\"${id}\"] is not a cron job: it needs a \\`schedule\\` string and a \\`handler\\` function. ` +\n \"Author it with `defineCron({ schedule, handler })`.\"\n );\n }\n return {\n id,\n definition: {\n ...def,\n schedule: def.schedule,\n name: def.name || id,\n enabled: def.enabled !== false,\n // `??`, not `||`: zero and NaN are values the author wrote, and\n // the scheduler refuses both by name (they end up under\n // `rejected`). `|| 300` swallowed them, so a job written with\n // `0` meaning \"no limit\" ran with five minutes and was listed as\n // healthy. `Infinity` is \"none\".\n timeoutSeconds: def.timeoutSeconds ?? 300,\n handler: def.handler\n }\n };\n });\n}\n\n/**\n * @deprecated Use `loadCrons` instead, which returns the jobs as a record keyed\n * by id. Will be removed in a future release.\n */\nexport async function loadCronJobsFromDirectory(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedCronJob[]> {\n warnDeprecated(\n \"server.loadCronJobsFromDirectory\",\n \"`loadCronJobsFromDirectory` is deprecated; use `loadCrons(dir)`, which returns `{ [id]: definition }`.\"\n );\n return (await legacyLoad(directory, importModule)).jobs;\n}\n\n/**\n * @deprecated Use `loadCrons` instead, with `onSkip` to collect the files it\n * skipped — and `declareCrons` where the jobs belong in the resource graph,\n * which this used to do as a side effect of loading. Will be removed in a\n * future release.\n */\nexport async function loadCronJobsWithDiagnostics(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedCronJobs> {\n warnDeprecated(\n \"server.loadCronJobsWithDiagnostics\",\n \"`loadCronJobsWithDiagnostics` is deprecated; use `loadCrons(dir, { onSkip })`, which returns \" +\n \"`{ [id]: definition }`, and `declareCrons(crons)` to record them in the resource graph.\"\n );\n return legacyLoad(directory, importModule);\n}\n\n/**\n * What the deprecated loaders did: load, declare every job in the resource\n * graph, and hand back normalised jobs. A job the graph refuses is skipped as a\n * problem, as it was when the declaration ran inside the loader.\n */\nasync function legacyLoad(directory: string, importModule: ModuleImporter): Promise<LoadedCronJobs> {\n const { crons, problems } = await readCronsDirectory(directory, importModule);\n const declared: CronsRecord = {};\n for (const [id, definition] of Object.entries(crons)) {\n try {\n declareCron(id, cronResourceOptions(definition));\n declared[id] = definition;\n } catch (err) {\n problems.push(describeLoadFailure(id, err));\n }\n }\n return { jobs: cronJobsFrom(declared), problems };\n}\n\n/**\n * Read a directory of cron files: the definitions that loaded, keyed by id, and\n * one `<file> (<reason>)` per file that will NOT be scheduled.\n *\n * A job that does not load does not run, and the only trace used to be one\n * `warn` per file in the boot log — next to nothing at 3am when the report that\n * should have been mailed was not. So the skips are counted, summarised in one\n * place, and answered by `GET /api/admin/cron`.\n *\n * Never throws, for the same reason the function loader does not: one\n * malformed file must not take the server down, and the other jobs in the\n * directory are still worth scheduling.\n */\nexport async function readCronsDirectory(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<{ crons: CronsRecord; problems: string[] }> {\n const crons: CronsRecord = {};\n // Aggregated so a broken job surfaces as one loud summary rather than a\n // warning buried per-file among the boot noise.\n const problems: string[] = [];\n\n if (!fs.existsSync(directory)) {\n return { crons, problems };\n }\n\n const files = fs.readdirSync(directory);\n for (const file of files) {\n if (\n (file.endsWith(\".ts\") || file.endsWith(\".js\")) &&\n // Dotfiles: notably macOS bsdtar AppleDouble sidecars (`._foo.ts`),\n // binary blobs that cannot be imported.\n !file.startsWith(\".\") &&\n !file.includes(\".test.\") &&\n !file.endsWith(\".d.ts\") &&\n file !== \"index.ts\" &&\n file !== \"index.js\"\n ) {\n const filePath = path.join(directory, file);\n try {\n const fileUrl = pathToFileURL(filePath).href;\n\n const mod = await importModule(fileUrl);\n\n const exported: unknown = mod.default;\n\n if (!exported || typeof exported !== \"object\") {\n problems.push(`${file} (no default export)`);\n logger.warn(`[cron] ${file}: no valid default export. Skipping.`);\n continue;\n }\n\n const def = exported as Record<string, unknown>;\n if (typeof def.schedule !== \"string\" || def.schedule.trim() === \"\" || typeof def.handler !== \"function\") {\n problems.push(`${file} (default export has no 'schedule' or no 'handler')`);\n logger.warn(`[cron] ${file}: default export missing required 'schedule' or 'handler'. Skipping.`);\n continue;\n }\n\n const id = path.basename(file, path.extname(file));\n const definition = exported as CronJobDefinition;\n crons[id] = definition;\n logger.info(\n `⏰ Loaded cron job: ${id} (${definition.schedule}` +\n `${definition.timezone ? ` ${definition.timezone}` : \" in the process's local time\"})`\n );\n } catch (err: unknown) {\n const message =\n err instanceof Error ? err.message : String(err);\n problems.push(describeLoadFailure(file, err));\n // At `debug`: the aggregate warning below lists every problem\n // by file, so this line was the same text a second time — and\n // when the throw is a Zod error, that text is ten lines of\n // serialised issues. The reader met it twice before reaching\n // the sentence that says what to do.\n logger.debug(`[cron] Failed to load ${file}: ${message}`);\n }\n }\n }\n\n if (problems.length > 0) {\n // Advice keyed to what went wrong, as the functions loader does:\n // `defineCron` is the remedy for a wrong export shape, not for a typo.\n const syntax = problems.some(p => p.includes(\"(syntax error\"));\n const other = problems.some(p => !p.includes(\"(syntax error\"));\n logger.warn(\n `[cron] ${problems.length} cron file(s) were skipped and will NOT be scheduled:\\n` +\n problems.map((p) => ` - ${p}`).join(\"\\n\") + \"\\n\" +\n [\n ...(syntax ? [SYNTAX_ERROR_ADVICE] : []),\n ...(other ? [\" Fix these or author them with `defineCron(...)` for a typed, compile-checked contract.\"] : [])\n ].join(\"\\n\")\n );\n }\n\n return { crons, problems };\n}\n\n/**\n * The declaration a definition makes: every field a host can read without the\n * handler. Spelled once here so the graph and the scheduler cannot disagree\n * about what a cron is.\n */\nexport function cronResourceOptions(definition: CronJobDefinition): CronResourceOptions {\n return {\n schedule: definition.schedule,\n ...(definition.timezone !== undefined ? { timezone: definition.timezone } : {}),\n ...(definition.description !== undefined ? { description: definition.description } : {}),\n ...(definition.enabled !== undefined ? { enabled: definition.enabled } : {}),\n ...(definition.timeoutSeconds !== undefined ? { timeoutSeconds: definition.timeoutSeconds } : {}),\n ...(definition.catchUpWindowSeconds !== undefined ? { catchUpWindowSeconds: definition.catchUpWindowSeconds } : {}),\n ...(definition.name ? { label: definition.name } : {})\n };\n}\n"],"mappings":"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"}