@spfn/core 0.3.0-beta.3 → 0.3.0-beta.5

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.
@@ -10,8 +10,13 @@ import { b as EventRouterDef, E as EventDef } from '../token-manager-BT5EnUAR.js
10
10
  import { d as SSEHandlerConfig, e as SSEAuthConfig } from '../types-ZQODsBft.js';
11
11
  import { W as WSRouterDef, f as WSHandlerConfig, e as WSMessageHandlers, g as WSAuthConfig } from '../types-2AbaW4Ie.js';
12
12
  import { DatabaseProvider, MigrationStatus, MigrationStatusDb } from '@spfn/core/db';
13
- import '@sinclair/typebox';
13
+ import * as _sinclair_typebox from '@sinclair/typebox';
14
+ import { Static } from '@sinclair/typebox';
15
+ import { R as RouteDef } from '../route-builder-2ani2jEI.js';
14
16
  import 'pg-boss';
17
+ import '../define-middleware-CVKgqo8S.js';
18
+ import 'hono/utils/http-status';
19
+ import '../route/types.js';
15
20
 
16
21
  /**
17
22
  * @deprecated Use `loadEnv` from '@spfn/core/env/loader' instead.
@@ -22,6 +27,33 @@ import 'pg-boss';
22
27
  */
23
28
  declare function loadEnvFiles(): void;
24
29
 
30
+ /** Stable operation identity used by separately deployed clients. */
31
+ declare const CORE_TIME_OPERATION_ID = "core.time";
32
+ /**
33
+ * Closed response shape for the server-time wire capability.
34
+ *
35
+ * Millisecond timestamps use integers throughout SPFN's external contracts.
36
+ */
37
+ declare const ServerTimeResponseSchema: _sinclair_typebox.TObject<{
38
+ serverTimeMillis: _sinclair_typebox.TInteger;
39
+ }>;
40
+ type ServerTimeResponse = Static<typeof ServerTimeResponseSchema>;
41
+ /** Injectable source of Unix epoch milliseconds. */
42
+ interface ServerClock {
43
+ now(): number;
44
+ }
45
+ /**
46
+ * Build the route from a clock so tests and alternate runtimes can supply the
47
+ * epoch source without replacing global time.
48
+ */
49
+ declare function createCoreTimeRoute(clock?: ServerClock): RouteDef<{}, {}, {
50
+ serverTimeMillis: number;
51
+ }>;
52
+ /** The production route and the public wire-contract source of truth. */
53
+ declare const CORE_TIME_ROUTE: RouteDef<{}, {}, {
54
+ serverTimeMillis: number;
55
+ }>;
56
+
25
57
  /**
26
58
  * Workflow router interface for @spfn/core integration
27
59
  *
@@ -489,6 +521,15 @@ interface ServerConfig {
489
521
  */
490
522
  detailed?: boolean;
491
523
  };
524
+ /**
525
+ * Server-time capability dependencies.
526
+ *
527
+ * The endpoint is always enabled at `GET /_core/time`. Supplying a clock is
528
+ * primarily a deterministic test seam; production defaults to `Date.now()`.
529
+ */
530
+ serverTime?: {
531
+ clock?: ServerClock;
532
+ };
492
533
  /**
493
534
  * Migration boot gate
494
535
  *
@@ -757,6 +798,13 @@ declare const CORE_NAMESPACE = "/_core";
757
798
  * balancer console — and a version bump migrates none of them.
758
799
  */
759
800
  declare const CORE_HEALTH_PATH = "/_core/health";
801
+ /**
802
+ * Where clients obtain the server's current Unix epoch in milliseconds.
803
+ *
804
+ * This endpoint is registered before application routes and application auth
805
+ * middleware so a client can call it before it has a proof or session.
806
+ */
807
+ declare const CORE_TIME_PATH = "/_core/time";
760
808
 
761
809
  /**
762
810
  * Start SPFN Server
@@ -1197,6 +1245,12 @@ declare class ServerConfigBuilder {
1197
1245
  * Configure health check endpoint
1198
1246
  */
1199
1247
  healthCheck(healthCheck: ServerConfig['healthCheck']): this;
1248
+ /**
1249
+ * Supply the clock used by the built-in `GET /_core/time` capability.
1250
+ * Production servers normally keep the default `Date.now()` clock; this is
1251
+ * exposed so tests can assert an exact wire value without replacing globals.
1252
+ */
1253
+ serverTime(serverTime: ServerConfig['serverTime']): this;
1200
1254
  /**
1201
1255
  * Configure infrastructure initialization
1202
1256
  */
@@ -1273,4 +1327,4 @@ declare class ServerConfigBuilder {
1273
1327
  */
1274
1328
  declare function defineServerConfig(): ServerConfigBuilder;
1275
1329
 
1276
- export { type AppFactory, CORE_HEALTH_PATH, CORE_NAMESPACE, type MigrationSnapshot, PendingMigrationsError, type ServerConfig, type ServerInstance, type ShutdownHookOptions, createServer, createServerlessApp, defineServerConfig, getMigrationSnapshot, getShutdownManager, loadEnvFiles, provisionInfrastructure, resetMigrationSnapshot, resetServerlessApp, startServer };
1330
+ export { type AppFactory, CORE_HEALTH_PATH, CORE_NAMESPACE, CORE_TIME_OPERATION_ID, CORE_TIME_PATH, CORE_TIME_ROUTE, type MigrationSnapshot, PendingMigrationsError, type ServerClock, type ServerConfig, type ServerInstance, type ServerTimeResponse, ServerTimeResponseSchema, type ShutdownHookOptions, createCoreTimeRoute, createServer, createServerlessApp, defineServerConfig, getMigrationSnapshot, getShutdownManager, loadEnvFiles, provisionInfrastructure, resetMigrationSnapshot, resetServerlessApp, startServer };
@@ -13,6 +13,7 @@ import { randomBytes, createHash } from 'crypto';
13
13
  import { Agent, setGlobalDispatcher } from 'undici';
14
14
  import { initDatabase, getDatabase, hasMigrationTargets, collectMigrationStatus, countPendingMigrations, pendingMigrationTargets, formatPendingMigrations, pendingMigrationsSummary, RUN_MIGRATIONS_HINT, closeDatabase, migrationTargets } from '@spfn/core/db';
15
15
  import { initCache, getCache, closeCache } from '@spfn/core/cache';
16
+ import { FormatRegistry, Type } from '@sinclair/typebox';
16
17
  import { serve } from '@hono/node-server';
17
18
  import PgBoss from 'pg-boss';
18
19
  import { networkInterfaces } from 'os';
@@ -354,9 +355,10 @@ function parseEnvFile(filePath) {
354
355
  return parse(readFileSync(filePath, "utf-8"));
355
356
  }
356
357
  function loadEnv(options = {}) {
358
+ const nodeEnvAtEntry = process.env.NODE_ENV;
357
359
  const {
358
360
  cwd = process.cwd(),
359
- nodeEnv = process.env.NODE_ENV || "local",
361
+ nodeEnv = nodeEnvAtEntry || "local",
360
362
  server = true,
361
363
  debug = false,
362
364
  override = false
@@ -386,8 +388,30 @@ function loadEnv(options = {}) {
386
388
  envLogger.debug(`Loaded env files: ${loadedFiles.join(", ")}`);
387
389
  envLogger.debug(`Loaded ${loadedKeys.length} environment variables`);
388
390
  }
391
+ warnIfEnvGuessCostSomething(cwd, nodeEnvAtEntry, options.nodeEnv, nodeEnv);
389
392
  return { loadedFiles, loadedKeys };
390
393
  }
394
+ function warnIfEnvGuessCostSomething(cwd, nodeEnvAtEntry, explicitNodeEnv, resolvedNodeEnv) {
395
+ if (nodeEnvAtEntry || explicitNodeEnv) {
396
+ return;
397
+ }
398
+ const declared = process.env.NODE_ENV;
399
+ if (!declared) {
400
+ envLogger.warn(
401
+ `NODE_ENV is not set anywhere; env files were resolved for "${resolvedNodeEnv}". Set NODE_ENV in the process environment to choose the file set.`
402
+ );
403
+ return;
404
+ }
405
+ if (declared === resolvedNodeEnv) {
406
+ return;
407
+ }
408
+ if (!existsSync(resolve(cwd, `.env.${declared}`))) {
409
+ return;
410
+ }
411
+ envLogger.warn(
412
+ `.env.${declared} was NOT loaded: NODE_ENV was not set when the process started, so env files were resolved for "${resolvedNodeEnv}" before a file declared NODE_ENV=${declared}. Set NODE_ENV in the process environment, not in a .env file.`
413
+ );
414
+ }
391
415
 
392
416
  // src/server/dotenv-loader.ts
393
417
  var warned = false;
@@ -1080,6 +1104,7 @@ async function runMigrationBootGate(config, cwd = process.cwd()) {
1080
1104
  // src/server/namespace.ts
1081
1105
  var CORE_NAMESPACE = "/_core";
1082
1106
  var CORE_HEALTH_PATH = `${CORE_NAMESPACE}/health`;
1107
+ var CORE_TIME_PATH = `${CORE_NAMESPACE}/time`;
1083
1108
  var LEGACY_HEALTH_PATH = "/health";
1084
1109
 
1085
1110
  // src/server/shutdown-manager.ts
@@ -1480,6 +1505,378 @@ function buildStartupConfig(config, timeouts) {
1480
1505
  };
1481
1506
  }
1482
1507
 
1508
+ // src/route/route-builder.ts
1509
+ var RouteBuilder = class _RouteBuilder {
1510
+ _method;
1511
+ _path;
1512
+ _input;
1513
+ _interceptor;
1514
+ _middlewares;
1515
+ _skipMiddlewares;
1516
+ _contract;
1517
+ /**
1518
+ * Create a new RouteBuilder with copied properties and optional overrides
1519
+ */
1520
+ clone(overrides) {
1521
+ const builder = new _RouteBuilder();
1522
+ builder._method = this._method;
1523
+ builder._path = this._path;
1524
+ builder._input = overrides?.input ?? this._input;
1525
+ builder._interceptor = overrides?.interceptor ?? this._interceptor;
1526
+ builder._middlewares = overrides?.middlewares ?? this._middlewares;
1527
+ builder._skipMiddlewares = overrides?.skipMiddlewares ?? this._skipMiddlewares;
1528
+ builder._contract = overrides?.contract ?? this._contract;
1529
+ return builder;
1530
+ }
1531
+ /**
1532
+ * Define input schemas
1533
+ *
1534
+ * @example
1535
+ * ```ts
1536
+ * route.get('/users/:id')
1537
+ * .input({
1538
+ * params: Type.Object({ id: Type.String() }),
1539
+ * query: Type.Object({ page: Type.Number() }),
1540
+ * headers: Type.Object({ authorization: Type.String() })
1541
+ * })
1542
+ * .handler(async (c) => {
1543
+ * const { params, query, headers } = await c.data();
1544
+ * // params = { id: string }
1545
+ * // query = { page: number }
1546
+ * // headers = { authorization: string }
1547
+ * })
1548
+ * ```
1549
+ */
1550
+ input(input) {
1551
+ return this.clone({ input });
1552
+ }
1553
+ /**
1554
+ * Define fields injected by interceptors
1555
+ *
1556
+ * These fields are:
1557
+ * - Available in the handler (merged with input)
1558
+ * - Excluded from client types (codegen uses only input)
1559
+ * - Not validated by route input schema (injected by middleware)
1560
+ *
1561
+ * Use this when middleware/interceptors add fields to the request
1562
+ * before it reaches the handler.
1563
+ *
1564
+ * @example
1565
+ * ```ts
1566
+ * // Auth interceptor injects crypto key fields
1567
+ * route.post('/_auth/login')
1568
+ * .input({
1569
+ * body: Type.Object({
1570
+ * email: Type.String(),
1571
+ * password: Type.String()
1572
+ * })
1573
+ * })
1574
+ * .interceptor({
1575
+ * body: Type.Object({
1576
+ * publicKey: Type.String(),
1577
+ * keyId: Type.String(),
1578
+ * fingerprint: Type.String()
1579
+ * })
1580
+ * })
1581
+ * .handler(async (c) => {
1582
+ * const { body } = await c.data();
1583
+ * // body type: { email, password, publicKey, keyId, fingerprint }
1584
+ * // Client only sees: { email, password }
1585
+ * return loginService(body);
1586
+ * });
1587
+ * ```
1588
+ */
1589
+ interceptor(interceptor) {
1590
+ return this.clone({ interceptor });
1591
+ }
1592
+ /**
1593
+ * Add middlewares to the route
1594
+ *
1595
+ * Accepts both regular middleware handlers and named middlewares (NamedMiddleware).
1596
+ * Named middlewares that are already registered globally will be automatically
1597
+ * deduplicated to prevent double execution.
1598
+ *
1599
+ * @example
1600
+ * ```ts
1601
+ * import { authenticate } from '@spfn/auth/server/middleware';
1602
+ *
1603
+ * // With NamedMiddleware (auto-deduped if registered globally)
1604
+ * route.get('/users')
1605
+ * .use([authenticate, RateLimitMiddleware()])
1606
+ *
1607
+ * // With regular middleware handlers
1608
+ * route.get('/users')
1609
+ * .use([AuthMiddleware(), RateLimitMiddleware()])
1610
+ * ```
1611
+ */
1612
+ middleware(middlewares) {
1613
+ return this.clone({ middlewares });
1614
+ }
1615
+ /**
1616
+ * Add middlewares to the route (alias for `.middleware()`)
1617
+ *
1618
+ * Accepts both regular middleware handlers and named middlewares (NamedMiddleware).
1619
+ * Named middlewares that are already registered globally will be automatically
1620
+ * deduplicated to prevent double execution.
1621
+ *
1622
+ * @example
1623
+ * ```ts
1624
+ * import { authenticate } from '@spfn/auth/server/middleware';
1625
+ *
1626
+ * // With NamedMiddleware (auto-deduped if registered globally)
1627
+ * route.get('/users')
1628
+ * .use([authenticate, RateLimitMiddleware()])
1629
+ *
1630
+ * // With regular middleware handlers
1631
+ * route.get('/users')
1632
+ * .use([AuthMiddleware(), RateLimitMiddleware()])
1633
+ * ```
1634
+ */
1635
+ use(middlewares) {
1636
+ return this.middleware(middlewares);
1637
+ }
1638
+ /**
1639
+ * Skip server-level named middlewares
1640
+ *
1641
+ * Useful for public endpoints that should bypass auth or rate limiting
1642
+ *
1643
+ * @param middlewareNames - Array of middleware names to skip, or '*' to skip all
1644
+ *
1645
+ * @example
1646
+ * ```ts
1647
+ * // Skip specific middlewares
1648
+ * route.get('/status')
1649
+ * .skip(['auth', 'rateLimit'])
1650
+ * .handler(async (c) => c.json({ status: 'ok' }));
1651
+ *
1652
+ * // Skip only auth (still apply rate limiting)
1653
+ * route.get('/public-data')
1654
+ * .skip(['auth'])
1655
+ * .handler(async (c) => { ... });
1656
+ *
1657
+ * // Skip all middlewares
1658
+ * route.get('/public-health')
1659
+ * .skip('*')
1660
+ * .handler(async (c) => c.json({ status: 'ok' }));
1661
+ * ```
1662
+ */
1663
+ skip(middlewareNames) {
1664
+ return this.clone({ skipMiddlewares: middlewareNames });
1665
+ }
1666
+ /**
1667
+ * Publish this route as a versioned contract operation
1668
+ *
1669
+ * Marks the route as a promise to clients that are compiled and deployed
1670
+ * separately from the server — a mobile app, an external API consumer.
1671
+ * The `@spfn/core:contract` generator writes every contracted route into
1672
+ * `contracts/current.json`, and the build refuses a change that would break
1673
+ * an already-released client.
1674
+ *
1675
+ * Routes without `.contract()` are unaffected: they simply do not appear in
1676
+ * the contract. A web client needs nothing here — it derives its types from
1677
+ * the router in the same build.
1678
+ *
1679
+ * @example
1680
+ * ```ts
1681
+ * export const getUser = route.get('/users/:id')
1682
+ * .input({ params: Type.Object({ id: Type.String() }) })
1683
+ * .contract({
1684
+ * since: '1.2.0',
1685
+ * auth: 'clientProofV1',
1686
+ * requiresSession: true,
1687
+ * response: Type.Object({
1688
+ * id: Type.String(),
1689
+ * name: Type.String(),
1690
+ * email: Type.Optional(Type.String()),
1691
+ * }),
1692
+ * })
1693
+ * .handler(async (c) => { ... });
1694
+ * ```
1695
+ */
1696
+ contract(contract) {
1697
+ return this.clone({ contract });
1698
+ }
1699
+ /**
1700
+ * Define handler function
1701
+ *
1702
+ * Response type is automatically inferred from the return value.
1703
+ * Use helper methods like `c.created()`, `c.paginated()` for proper type inference.
1704
+ *
1705
+ * @example
1706
+ * ```ts
1707
+ * // Direct return - type inferred from data
1708
+ * route.get('/users/:id')
1709
+ * .input({ params: Type.Object({ id: Type.String() }) })
1710
+ * .handler(async (c) => {
1711
+ * const { params } = await c.data();
1712
+ * return await getUser(params.id); // Type: User
1713
+ * })
1714
+ *
1715
+ * // Using c.created() - returns data with 201 status, type preserved
1716
+ * route.post('/users')
1717
+ * .input({ body: Type.Object({ name: Type.String() }) })
1718
+ * .handler(async (c) => {
1719
+ * const { body } = await c.data();
1720
+ * return c.created(await createUser(body)); // Type: User
1721
+ * })
1722
+ *
1723
+ * // Using c.paginated() - returns PaginatedResult<T>
1724
+ * route.get('/users')
1725
+ * .handler(async (c) => {
1726
+ * const users = await getUsers();
1727
+ * return c.paginated(users, 1, 20, 100); // Type: PaginatedResult<User>
1728
+ * })
1729
+ *
1730
+ * // Using c.noContent() - returns void
1731
+ * route.delete('/users/:id')
1732
+ * .handler(async (c) => {
1733
+ * await deleteUser(params.id);
1734
+ * return c.noContent(); // Type: void
1735
+ * })
1736
+ *
1737
+ * // Using c.json() - returns Response (type inference lost)
1738
+ * // Use only when you need custom status codes not covered by helpers
1739
+ * route.get('/custom')
1740
+ * .handler(async (c) => {
1741
+ * return c.json({ data }, 418); // Type: Response
1742
+ * })
1743
+ * ```
1744
+ */
1745
+ handler(fn) {
1746
+ return {
1747
+ method: this._method,
1748
+ path: this._path,
1749
+ input: this._input,
1750
+ interceptor: this._interceptor,
1751
+ middlewares: this._middlewares,
1752
+ skipMiddlewares: this._skipMiddlewares,
1753
+ contract: this._contract,
1754
+ handler: fn,
1755
+ _input: {},
1756
+ _interceptor: {},
1757
+ _response: {}
1758
+ };
1759
+ }
1760
+ };
1761
+ function createMethodRoute(method) {
1762
+ return (path) => {
1763
+ const builder = new RouteBuilder();
1764
+ builder._method = method;
1765
+ builder._path = path;
1766
+ return builder;
1767
+ };
1768
+ }
1769
+ var route = {
1770
+ get: createMethodRoute("GET"),
1771
+ post: createMethodRoute("POST"),
1772
+ put: createMethodRoute("PUT"),
1773
+ patch: createMethodRoute("PATCH"),
1774
+ delete: createMethodRoute("DELETE")
1775
+ };
1776
+
1777
+ // src/route/router.ts
1778
+ function createRouterInstance(routes, packageRouters = [], globalMiddlewares = [], contractVersion = null) {
1779
+ return {
1780
+ routes,
1781
+ _routes: routes,
1782
+ _packageRouters: packageRouters,
1783
+ _globalMiddlewares: globalMiddlewares,
1784
+ _contractVersion: contractVersion,
1785
+ packages(routers) {
1786
+ const newPackageRouters = [...this._packageRouters, ...routers];
1787
+ for (const pkgRouter of routers) {
1788
+ if (pkgRouter._packageRouters?.length > 0) {
1789
+ newPackageRouters.push(...pkgRouter._packageRouters);
1790
+ }
1791
+ }
1792
+ return createRouterInstance(
1793
+ this.routes,
1794
+ newPackageRouters,
1795
+ this._globalMiddlewares,
1796
+ this._contractVersion
1797
+ );
1798
+ },
1799
+ use(middlewares) {
1800
+ return createRouterInstance(
1801
+ this.routes,
1802
+ this._packageRouters,
1803
+ [...this._globalMiddlewares, ...middlewares],
1804
+ this._contractVersion
1805
+ );
1806
+ },
1807
+ contractVersion(version) {
1808
+ assertContractVersion(version);
1809
+ return createRouterInstance(
1810
+ this.routes,
1811
+ this._packageRouters,
1812
+ this._globalMiddlewares,
1813
+ version
1814
+ );
1815
+ }
1816
+ };
1817
+ }
1818
+ function assertContractVersion(version) {
1819
+ if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/.test(version)) {
1820
+ throw new Error(
1821
+ `contractVersion("${version}") is not a version of the form major.minor.patch. The released snapshot is named from this value and releases are compared by it.`
1822
+ );
1823
+ }
1824
+ }
1825
+ function defineRouter(routes) {
1826
+ return createRouterInstance(routes);
1827
+ }
1828
+
1829
+ // src/route/validation.ts
1830
+ FormatRegistry.Set(
1831
+ "email",
1832
+ (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
1833
+ );
1834
+ FormatRegistry.Set(
1835
+ "uri",
1836
+ (value) => /^https?:\/\/.+/.test(value)
1837
+ );
1838
+ FormatRegistry.Set(
1839
+ "uuid",
1840
+ (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
1841
+ );
1842
+ FormatRegistry.Set(
1843
+ "date",
1844
+ (value) => /^\d{4}-\d{2}-\d{2}$/.test(value)
1845
+ );
1846
+ FormatRegistry.Set(
1847
+ "date-time",
1848
+ (value) => !isNaN(Date.parse(value))
1849
+ );
1850
+
1851
+ // src/server/server-time.ts
1852
+ var CORE_TIME_OPERATION_ID = "core.time";
1853
+ var ServerTimeResponseSchema = Type.Object({
1854
+ serverTimeMillis: Type.Integer()
1855
+ }, { additionalProperties: false });
1856
+ var systemServerClock = {
1857
+ now: () => Date.now()
1858
+ };
1859
+ function createCoreTimeRoute(clock = systemServerClock) {
1860
+ return route.get(CORE_TIME_PATH).skip("*").contract({
1861
+ since: "0.3.0",
1862
+ auth: "none",
1863
+ requiresSession: false,
1864
+ response: ServerTimeResponseSchema
1865
+ }).handler(async (c) => {
1866
+ const response = {
1867
+ serverTimeMillis: clock.now()
1868
+ };
1869
+ c.raw.header("Cache-Control", "no-store");
1870
+ return response;
1871
+ });
1872
+ }
1873
+ var CORE_TIME_ROUTE = createCoreTimeRoute();
1874
+ function createCoreTimeRouter(clock) {
1875
+ return defineRouter({
1876
+ [CORE_TIME_OPERATION_ID]: createCoreTimeRoute(clock)
1877
+ });
1878
+ }
1879
+
1483
1880
  // src/server/create-server.ts
1484
1881
  var rateLimitApplied = /* @__PURE__ */ new WeakSet();
1485
1882
  async function createServer(config) {
@@ -1522,6 +1919,7 @@ async function createAutoConfiguredApp(config) {
1522
1919
  applyDefaultMiddleware(app, config, enableLogger, enableCors);
1523
1920
  await applyProxyGuard(app, config);
1524
1921
  applyRateLimit(config);
1922
+ registerCoreTimeEndpoint(app, config);
1525
1923
  if (Array.isArray(config?.use)) {
1526
1924
  config.use.forEach((mw) => app.use("*", mw));
1527
1925
  }
@@ -1571,7 +1969,7 @@ async function applyProxyGuard(app, config) {
1571
1969
  serverLogger.warn("Proxy-guard nonce: cache module unavailable \u2014 using in-memory store (single instance only)");
1572
1970
  }
1573
1971
  }
1574
- const autoSkip = [CORE_HEALTH_PATH, LEGACY_HEALTH_PATH];
1972
+ const autoSkip = [CORE_HEALTH_PATH, CORE_TIME_PATH, LEGACY_HEALTH_PATH];
1575
1973
  if (config?.healthCheck?.path) {
1576
1974
  autoSkip.push(config.healthCheck.path);
1577
1975
  }
@@ -1628,6 +2026,10 @@ function resolveHealthCheck(config) {
1628
2026
  detailed: healthCheckConfig.detailed ?? process.env.NODE_ENV === "development"
1629
2027
  };
1630
2028
  }
2029
+ function registerCoreTimeEndpoint(app, config) {
2030
+ registerRoutes(app, createCoreTimeRouter(config?.serverTime?.clock));
2031
+ serverLogger.debug(`Server time endpoint enabled at ${CORE_TIME_PATH}`);
2032
+ }
1631
2033
  function registerCoreHealthEndpoint(app, config) {
1632
2034
  const { enabled, path, detailed } = resolveHealthCheck(config);
1633
2035
  if (!enabled) {
@@ -3231,6 +3633,15 @@ var ServerConfigBuilder = class {
3231
3633
  this.config.healthCheck = healthCheck;
3232
3634
  return this;
3233
3635
  }
3636
+ /**
3637
+ * Supply the clock used by the built-in `GET /_core/time` capability.
3638
+ * Production servers normally keep the default `Date.now()` clock; this is
3639
+ * exposed so tests can assert an exact wire value without replacing globals.
3640
+ */
3641
+ serverTime(serverTime) {
3642
+ this.config.serverTime = serverTime;
3643
+ return this;
3644
+ }
3234
3645
  /**
3235
3646
  * Configure infrastructure initialization
3236
3647
  */
@@ -3327,6 +3738,6 @@ function defineServerConfig() {
3327
3738
  return new ServerConfigBuilder();
3328
3739
  }
3329
3740
 
3330
- export { CORE_HEALTH_PATH, CORE_NAMESPACE, PendingMigrationsError, createServer, createServerlessApp, defineServerConfig, getMigrationSnapshot, getShutdownManager, loadEnv, loadEnvFiles, provisionInfrastructure, resetMigrationSnapshot, resetServerlessApp, startServer };
3741
+ export { CORE_HEALTH_PATH, CORE_NAMESPACE, CORE_TIME_OPERATION_ID, CORE_TIME_PATH, CORE_TIME_ROUTE, PendingMigrationsError, ServerTimeResponseSchema, createCoreTimeRoute, createServer, createServerlessApp, defineServerConfig, getMigrationSnapshot, getShutdownManager, loadEnv, loadEnvFiles, provisionInfrastructure, resetMigrationSnapshot, resetServerlessApp, startServer };
3331
3742
  //# sourceMappingURL=index.js.map
3332
3743
  //# sourceMappingURL=index.js.map