@rebasepro/server 0.16.0 → 0.16.1-canary.g0d7af95
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.
- package/dist/{GCSStorageController-BEmDYFKc.js → GCSStorageController-Bl7nmhdv.js} +7 -6
- package/dist/{GCSStorageController-BEmDYFKc.js.map → GCSStorageController-Bl7nmhdv.js.map} +1 -1
- package/dist/{S3StorageController-B2EDXXMH.js → S3StorageController-CWvvrRpF.js} +7 -6
- package/dist/{S3StorageController-B2EDXXMH.js.map → S3StorageController-CWvvrRpF.js.map} +1 -1
- package/dist/{admin_block-DLILvzle.js → admin_block-BeypnEfb.js} +6 -6
- package/dist/admin_block-BeypnEfb.js.map +1 -0
- package/dist/api/ast-schema-editor.d.ts +23 -0
- package/dist/api/contract-routes.d.ts +1 -1
- package/dist/api/errors.d.ts +1 -1
- package/dist/api/index.d.ts +3 -3
- package/dist/api/live-schema-routes.d.ts +122 -0
- package/dist/api/logs-routes.d.ts +1 -1
- package/dist/api/mount.d.ts +41 -0
- package/dist/api/rest/api-generator.d.ts +2 -2
- package/dist/api/rest/index.d.ts +1 -1
- package/dist/api/rest/query-parser.d.ts +1 -1
- package/dist/api/schema-editor-routes.d.ts +1 -1
- package/dist/api/types.d.ts +12 -4
- package/dist/{schema-editor-routes-CV9k0w3G.js → ast-schema-editor-BrarYZCq.js} +37 -45
- package/dist/ast-schema-editor-BrarYZCq.js.map +1 -0
- package/dist/auth/adapter-middleware.d.ts +2 -2
- package/dist/auth/admin-roles-route.d.ts +2 -2
- package/dist/auth/admin-user-ops.d.ts +3 -3
- package/dist/auth/admin-users-route.d.ts +4 -6
- package/dist/auth/api-keys/api-key-middleware.d.ts +2 -2
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +1 -1
- package/dist/auth/api-keys/api-key-routes.d.ts +2 -2
- package/dist/auth/api-keys/api-key-store.d.ts +1 -1
- package/dist/auth/api-keys/index.d.ts +9 -9
- package/dist/auth/apple-oauth.d.ts +2 -2
- package/dist/auth/auth-hooks.d.ts +4 -4
- package/dist/auth/bitbucket-oauth.d.ts +2 -2
- package/dist/auth/builtin-auth-adapter.d.ts +8 -4
- package/dist/auth/capabilities.d.ts +51 -0
- package/dist/auth/captcha.d.ts +86 -0
- package/dist/auth/cookie-utils.d.ts +2 -2
- package/dist/auth/discord-oauth.d.ts +2 -2
- package/dist/auth/facebook-oauth.d.ts +2 -2
- package/dist/auth/github-oauth.d.ts +2 -2
- package/dist/auth/gitlab-oauth.d.ts +2 -2
- package/dist/auth/google-oauth.d.ts +1 -1
- package/dist/auth/index.d.ts +58 -54
- package/dist/auth/jwks-routes.d.ts +1 -1
- package/dist/auth/jwt.d.ts +1 -1
- package/dist/auth/linkedin-oauth.d.ts +2 -2
- package/dist/auth/magic-link-routes.d.ts +10 -3
- package/dist/auth/mfa-gate.d.ts +1 -1
- package/dist/auth/mfa-routes.d.ts +3 -3
- package/dist/auth/microsoft-oauth.d.ts +2 -2
- package/dist/auth/middleware.d.ts +6 -7
- package/dist/auth/otp-routes.d.ts +89 -0
- package/dist/auth/rate-limiter.d.ts +2 -2
- package/dist/auth/require-auth.d.ts +1 -1
- package/dist/auth/reset-password-admin.d.ts +4 -4
- package/dist/auth/routes.d.ts +18 -10
- package/dist/auth/session-routes.d.ts +3 -3
- package/dist/auth/slack-oauth.d.ts +2 -2
- package/dist/auth/spotify-oauth.d.ts +2 -2
- package/dist/auth/token-revocation.d.ts +2 -2
- package/dist/auth/twitter-oauth.d.ts +2 -2
- package/dist/{auth-5Et5mnUA.js → auth-DU-nUjPp.js} +604 -4376
- package/dist/auth-DU-nUjPp.js.map +1 -0
- package/dist/backup/backup-common.d.ts +1 -1
- package/dist/backup/backup-routes.d.ts +3 -3
- package/dist/backup/index.d.ts +3 -3
- package/dist/{backup-C6ljYVTp.js → backup-CRdZkA6c.js} +7 -6
- package/dist/{backup-C6ljYVTp.js.map → backup-CRdZkA6c.js.map} +1 -1
- package/dist/boot/boot.d.ts +5 -5
- package/dist/boot/bundle.d.ts +10 -1
- package/dist/boot/driver.d.ts +1 -1
- package/dist/boot/env.d.ts +28 -1
- package/dist/boot/fetch-bundle.d.ts +144 -1
- package/dist/boot/options.d.ts +30 -6
- package/dist/boot/resource-adapters.d.ts +31 -0
- package/dist/boot/resource-loading.d.ts +10 -0
- package/dist/boot/role.d.ts +3 -2
- package/dist/boot/sources.d.ts +8 -2
- package/dist/boot/version-skew.d.ts +1 -1
- package/dist/collections/BackendCollectionRegistry.d.ts +1 -1
- package/dist/collections/loader.d.ts +1 -1
- package/dist/collections/validate-config.d.ts +13 -0
- package/dist/{contract-routes-DZ-LBpSL.js → contract-routes-D8TvxPLo.js} +8 -7
- package/dist/contract-routes-D8TvxPLo.js.map +1 -0
- package/dist/cron/cron-routes.d.ts +2 -2
- package/dist/cron/cron-scheduler.d.ts +3 -3
- package/dist/cron/index.d.ts +8 -8
- package/dist/{cron-loader-YhhQeVBM.js → cron-loader-d9WMFENB.js} +8 -7
- package/dist/{cron-loader-YhhQeVBM.js.map → cron-loader-d9WMFENB.js.map} +1 -1
- package/dist/{cron-routes-maM_RlUu.js → cron-routes-BsukhMGm.js} +9 -8
- package/dist/cron-routes-BsukhMGm.js.map +1 -0
- package/dist/{cron-scheduler-DIpYBmZP.js → cron-scheduler-BB82dWuU.js} +9 -9
- package/dist/cron-scheduler-BB82dWuU.js.map +1 -0
- package/dist/{cron-store-DfH_4Cd9.js → cron-store-CbFfQhbg.js} +11 -9
- package/dist/{cron-store-DfH_4Cd9.js.map → cron-store-CbFfQhbg.js.map} +1 -1
- package/dist/ddl-bootstrap-5YZCZ8qk.js +146 -0
- package/dist/ddl-bootstrap-5YZCZ8qk.js.map +1 -0
- package/dist/deploy/pod-contract.d.ts +139 -0
- package/dist/dev-secrets.d.ts +52 -0
- package/dist/{dynamic-import-Dvh-K5fl.js → dynamic-import-X40pTZUQ.js} +5 -4
- package/dist/{dynamic-import-Dvh-K5fl.js.map → dynamic-import-X40pTZUQ.js.map} +1 -1
- package/dist/email/dev-sink.d.ts +88 -0
- package/dist/email/index.d.ts +9 -7
- package/dist/email/link-base.d.ts +1 -1
- package/dist/email/smtp-email-service.d.ts +2 -2
- package/dist/email/templates.d.ts +12 -0
- package/dist/email/types.d.ts +18 -6
- package/dist/env.d.ts +1 -1
- package/dist/{errors-EBYiaJ2E.js → errors-DBwpj9N8.js} +8 -7
- package/dist/errors-DBwpj9N8.js.map +1 -0
- package/dist/{function-loader-DDS1v7YX.js → function-loader-LLdmBFoL.js} +9 -8
- package/dist/{function-loader-DDS1v7YX.js.map → function-loader-LLdmBFoL.js.map} +1 -1
- package/dist/{function-routes-Btcez1T-.js → function-routes-ClT6UQpD.js} +8 -7
- package/dist/function-routes-ClT6UQpD.js.map +1 -0
- package/dist/functions/context.d.ts +141 -0
- package/dist/functions/define-function.d.ts +1 -1
- package/dist/functions/function-routes.d.ts +9 -3
- package/dist/functions/guards.d.ts +76 -0
- package/dist/functions/index.d.ts +96 -5
- package/dist/functions/index.js +919 -0
- package/dist/functions/index.js.map +1 -0
- package/dist/functions/internal.d.ts +26 -0
- package/dist/functions/proxy.d.ts +1 -1
- package/dist/functions/request-timeout.d.ts +9 -1
- package/dist/functions/runtime-env.d.ts +92 -0
- package/dist/functions/wait-until.d.ts +74 -0
- package/dist/history/history-routes.d.ts +2 -2
- package/dist/history/index.d.ts +1 -1
- package/dist/history-recorder-B67maiTi.js +76 -0
- package/dist/history-recorder-B67maiTi.js.map +1 -0
- package/dist/history-store-oiqhb3HU.js +210 -0
- package/dist/history-store-oiqhb3HU.js.map +1 -0
- package/dist/index.d.ts +65 -54
- package/dist/index.es.js +3667 -328
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +1 -1
- package/dist/init/middlewares.d.ts +1 -1
- package/dist/init/shutdown.d.ts +4 -0
- package/dist/init/storage.d.ts +1 -1
- package/dist/init/surfaces.d.ts +10 -0
- package/dist/init.d.ts +179 -22
- package/dist/internal-tables-DpxfPaEB.js +97 -0
- package/dist/internal-tables-DpxfPaEB.js.map +1 -0
- package/dist/jobs/index.d.ts +5 -5
- package/dist/{jobs-CyOKXXlu.js → jobs-B2ELModo.js} +11 -9
- package/dist/{jobs-CyOKXXlu.js.map → jobs-B2ELModo.js.map} +1 -1
- package/dist/{jwt-DxH9fLPt.js → jwt-DoHkMMWF.js} +9 -8
- package/dist/jwt-DoHkMMWF.js.map +1 -0
- package/dist/{logger-DfvF_8r-.js → logger-DS03e908.js} +104 -10
- package/dist/logger-DS03e908.js.map +1 -0
- package/dist/{logs-routes-CWBLQj2l.js → logs-routes-DB72iQSr.js} +36 -9
- package/dist/logs-routes-DB72iQSr.js.map +1 -0
- package/dist/metrics/history-recorder.d.ts +18 -0
- package/dist/metrics/history-store.d.ts +123 -0
- package/dist/metrics/index.d.ts +10 -2
- package/dist/{openapi-generator-BCKJRUS4.js → openapi-generator-FFGT0H6O.js} +7 -11
- package/dist/{openapi-generator-BCKJRUS4.js.map → openapi-generator-FFGT0H6O.js.map} +1 -1
- package/dist/{proxy-Bj5DVllb.js → proxy-CMymhnwG.js} +9 -6
- package/dist/{proxy-Bj5DVllb.js.map → proxy-CMymhnwG.js.map} +1 -1
- package/dist/query-parser--nstjRCh.js +355 -0
- package/dist/query-parser--nstjRCh.js.map +1 -0
- package/dist/{request-timeout-BuFoEKwT.js → request-timeout-C8gkc-j7.js} +21 -6
- package/dist/request-timeout-C8gkc-j7.js.map +1 -0
- package/dist/rls-audit/index.d.ts +111 -0
- package/dist/{rolldown-runtime-DSJWtz9O.js → rolldown-runtime-dW7B1o5h.js} +4 -3
- package/dist/schema-edit/apply-schema-change.d.ts +141 -0
- package/dist/schema-edit/github-repository.d.ts +67 -0
- package/dist/schema-edit/local-git-repository.d.ts +28 -0
- package/dist/schema-edit/project-root.d.ts +43 -0
- package/dist/schema-edit/remote-source.d.ts +25 -0
- package/dist/schema-edit/schema-edit-permissions.d.ts +129 -0
- package/dist/schema-editor-routes-Bf5h5Emf.js +87 -0
- package/dist/schema-editor-routes-Bf5h5Emf.js.map +1 -0
- package/dist/schemas-DBxgjM9A.js +4153 -0
- package/dist/schemas-DBxgjM9A.js.map +1 -0
- package/dist/{selection-_z6TM1DB.js → selection-CRpqKUbt.js} +6 -5
- package/dist/{selection-_z6TM1DB.js.map → selection-CRpqKUbt.js.map} +1 -1
- package/dist/services/webhook-service.d.ts +1 -1
- package/dist/singleton.d.ts +7 -0
- package/dist/{src-BPfYOeN4.js → src-B-CmIFMr.js} +8 -6
- package/dist/src-B-CmIFMr.js.map +1 -0
- package/dist/{src-CrCxd8km.js → src-Cdsw7DqV.js} +126 -5
- package/dist/src-Cdsw7DqV.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +1 -1
- package/dist/storage/LocalStorageController.d.ts +1 -1
- package/dist/storage/S3StorageController.d.ts +1 -1
- package/dist/storage/cache-headers.d.ts +87 -0
- package/dist/storage/index.d.ts +11 -11
- package/dist/storage/path-pattern.d.ts +58 -0
- package/dist/storage/policies.d.ts +88 -0
- package/dist/storage/range.d.ts +63 -0
- package/dist/storage/rendition-cache.d.ts +45 -0
- package/dist/storage/routes.d.ts +17 -5
- package/dist/storage/storage-registry.d.ts +1 -1
- package/dist/storage/triggers.d.ts +66 -0
- package/dist/storage/tus-handler.d.ts +34 -3
- package/dist/topics/runtime.d.ts +68 -0
- package/dist/{types-DSnOC4mF.js → types-BfKcm9do.js} +5 -4
- package/dist/{types-DSnOC4mF.js.map → types-BfKcm9do.js.map} +1 -1
- package/dist/utils/host.d.ts +58 -0
- package/dist/utils/logger.d.ts +0 -15
- package/dist/utils/request-id.d.ts +1 -1
- package/functions/package.json +24 -0
- package/package.json +13 -7
- package/dist/admin_block-DLILvzle.js.map +0 -1
- package/dist/auth-5Et5mnUA.js.map +0 -1
- package/dist/contract-routes-DZ-LBpSL.js.map +0 -1
- package/dist/cron-routes-maM_RlUu.js.map +0 -1
- package/dist/cron-scheduler-DIpYBmZP.js.map +0 -1
- package/dist/ddl-bootstrap-Cywoj8Ta.js +0 -221
- package/dist/ddl-bootstrap-Cywoj8Ta.js.map +0 -1
- package/dist/errors-EBYiaJ2E.js.map +0 -1
- package/dist/function-routes-Btcez1T-.js.map +0 -1
- package/dist/jwt-DxH9fLPt.js.map +0 -1
- package/dist/logger-DfvF_8r-.js.map +0 -1
- package/dist/logs-routes-CWBLQj2l.js.map +0 -1
- package/dist/request-timeout-BuFoEKwT.js.map +0 -1
- package/dist/schema-editor-routes-CV9k0w3G.js.map +0 -1
- package/dist/src-BPfYOeN4.js.map +0 -1
- package/dist/src-CrCxd8km.js.map +0 -1
|
@@ -1,7 +1,101 @@
|
|
|
1
|
-
import { createRequire as
|
|
2
|
-
import
|
|
3
|
-
|
|
1
|
+
import { createRequire as __rebaseCreateRequire } from "module";
|
|
2
|
+
import __rebaseProcess from "process";
|
|
3
|
+
globalThis.process ??= __rebaseProcess;
|
|
4
|
+
__rebaseCreateRequire(import.meta.url);
|
|
5
|
+
//#region src/utils/host.ts
|
|
6
|
+
/**
|
|
7
|
+
* The host globals this package reads, behind functions that do not assume Node.
|
|
8
|
+
*
|
|
9
|
+
* `process` is not defined on workerd, on Deno Deploy without the compat flag,
|
|
10
|
+
* or in a browser. That matters here for one specific reason: the portable
|
|
11
|
+
* authoring surface (`@rebasepro/server/functions`) reaches the logger and the
|
|
12
|
+
* error handler, and a bare `process.env.NODE_ENV` inside either of them turns
|
|
13
|
+
* the first log line of a request into a `ReferenceError` on a runtime that has
|
|
14
|
+
* no `process` — a failure that reads as "the framework crashed" rather than
|
|
15
|
+
* "this runtime has no process object".
|
|
16
|
+
*
|
|
17
|
+
* Nothing here throws and nothing here is async. A runtime that cannot answer
|
|
18
|
+
* gets the empty answer, because every caller in this file's blast radius is
|
|
19
|
+
* choosing a log level or a format, and the safe default for both is the
|
|
20
|
+
* development one.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Where an adapter with no `process` can publish the environment.
|
|
26
|
+
*
|
|
27
|
+
* Cloudflare Workers hand the environment to the *request*, not to the module,
|
|
28
|
+
* so there is no global to read at import time. An edge adapter that has
|
|
29
|
+
* already seen a request can stash the bag here and every contextless reader in
|
|
30
|
+
* the framework — the logger, chiefly — starts answering correctly.
|
|
31
|
+
*
|
|
32
|
+
* `Symbol.for` rather than a module-local for the same reason the singleton
|
|
33
|
+
* uses it: more than one copy of this module can be loaded into one process,
|
|
34
|
+
* and a module-local would leave every copy but the writer's blind. See
|
|
35
|
+
* `../singleton.ts`.
|
|
36
|
+
*
|
|
37
|
+
* Request-scoped code should NOT read this. Use `getEnv(c)` from
|
|
38
|
+
* `@rebasepro/server/functions`, which reads the binding attached to the
|
|
39
|
+
* request it is serving — the only correct source on a runtime where two
|
|
40
|
+
* concurrent requests can carry different bindings.
|
|
41
|
+
*/
|
|
42
|
+
var ENV_SLOT = Symbol.for("@rebasepro/server:host-env");
|
|
43
|
+
/**
|
|
44
|
+
* The process environment, or the closest thing this runtime has to one.
|
|
45
|
+
*
|
|
46
|
+
* Order: a bag published by {@link setHostEnv} first, because an adapter that
|
|
47
|
+
* set one knows more than the ambient globals do; then `process.env`; then
|
|
48
|
+
* nothing.
|
|
49
|
+
*/
|
|
50
|
+
function hostEnv() {
|
|
51
|
+
const global = globalThis;
|
|
52
|
+
return global[ENV_SLOT] ?? global.process?.env ?? {};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Write one already-formatted line to the process's output.
|
|
56
|
+
*
|
|
57
|
+
* `process.stdout.write` is preferred where it exists because it is the only
|
|
58
|
+
* one of the two that does not append its own formatting to a line that is
|
|
59
|
+
* already a complete JSON document — `console.log` on Node is
|
|
60
|
+
* `process.stdout.write` plus `util.format`, and `util.format` will happily
|
|
61
|
+
* reinterpret a `%s` that appeared inside a user's log message.
|
|
62
|
+
*
|
|
63
|
+
* Where it does not exist, `console` is the runtime's log sink and is what its
|
|
64
|
+
* platform collects.
|
|
65
|
+
*/
|
|
66
|
+
function writeLine(stream, line) {
|
|
67
|
+
const proc = globalThis.process;
|
|
68
|
+
const sink = stream === "err" ? proc?.stderr : proc?.stdout;
|
|
69
|
+
if (typeof sink?.write === "function") {
|
|
70
|
+
sink.write(line + "\n");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (stream === "err") console.error(line);
|
|
74
|
+
else console.log(line);
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
4
77
|
//#region src/utils/logger.ts
|
|
78
|
+
/**
|
|
79
|
+
* Structured Logger for Rebase Backend
|
|
80
|
+
*
|
|
81
|
+
* Outputs JSON lines when `NODE_ENV=production`, human-readable prefixed
|
|
82
|
+
* lines otherwise. Designed to work with Google Cloud Logging severity levels.
|
|
83
|
+
*
|
|
84
|
+
* Every line — message and data, at any depth — passes through the redaction
|
|
85
|
+
* below, which strips Drizzle's `Failed query: … / params: …` wrapper and the
|
|
86
|
+
* values of secret-looking keys. See the block above `serialiseError`.
|
|
87
|
+
*
|
|
88
|
+
* Usage:
|
|
89
|
+
* import { logger } from "./utils/logger";
|
|
90
|
+
* logger.info("Server started", { port: 3001 });
|
|
91
|
+
* logger.error("Request failed", { path: "/api/test", error: err });
|
|
92
|
+
*
|
|
93
|
+
* Every host global goes through `./host`, and that is load-bearing rather than
|
|
94
|
+
* tidy: this module is reachable from `@rebasepro/server/functions`, the
|
|
95
|
+
* authoring surface that has to import cleanly on a runtime with no `process`.
|
|
96
|
+
* A bare `process.env.NODE_ENV` here would make the first log line of the first
|
|
97
|
+
* request on workerd a `ReferenceError`.
|
|
98
|
+
*/
|
|
5
99
|
/** Google Cloud Logging severity strings. */
|
|
6
100
|
var GCP_SEVERITY = {
|
|
7
101
|
debug: "DEBUG",
|
|
@@ -16,10 +110,10 @@ var LOG_PRIORITY = {
|
|
|
16
110
|
error: 3
|
|
17
111
|
};
|
|
18
112
|
function isProduction() {
|
|
19
|
-
return
|
|
113
|
+
return hostEnv().NODE_ENV === "production";
|
|
20
114
|
}
|
|
21
115
|
function getMinLevel() {
|
|
22
|
-
const env = (
|
|
116
|
+
const env = (hostEnv().LOG_LEVEL || "info").toLowerCase();
|
|
23
117
|
if (env in LOG_PRIORITY) return env;
|
|
24
118
|
return "info";
|
|
25
119
|
}
|
|
@@ -57,7 +151,7 @@ function isSensitiveKey(key) {
|
|
|
57
151
|
* it, and it never re-enables the key deny-list.
|
|
58
152
|
*/
|
|
59
153
|
function rawQueriesAllowed() {
|
|
60
|
-
return
|
|
154
|
+
return hostEnv().NODE_ENV !== "production" && hostEnv().REBASE_LOG_RAW_QUERIES === "true";
|
|
61
155
|
}
|
|
62
156
|
/**
|
|
63
157
|
* Strip every `Failed query: … / params: …` span out of a message or stack,
|
|
@@ -154,8 +248,8 @@ function createLogger(rawDefaultFields = {}) {
|
|
|
154
248
|
...merged
|
|
155
249
|
};
|
|
156
250
|
const line = JSON.stringify(entry);
|
|
157
|
-
if (level === "error")
|
|
158
|
-
else
|
|
251
|
+
if (level === "error") writeLine("err", line);
|
|
252
|
+
else writeLine("out", line);
|
|
159
253
|
} else {
|
|
160
254
|
const prefix = level === "error" ? "❌" : level === "warn" ? "⚠️" : level === "info" ? "ℹ️" : "🐛";
|
|
161
255
|
const extra = Object.keys(merged).length > 0 ? ` ${JSON.stringify(merged)}` : "";
|
|
@@ -185,6 +279,6 @@ function createLogger(rawDefaultFields = {}) {
|
|
|
185
279
|
*/
|
|
186
280
|
var logger = createLogger();
|
|
187
281
|
//#endregion
|
|
188
|
-
export { redactSensitiveText as n, logger as t };
|
|
282
|
+
export { redactSensitiveText as n, hostEnv as r, logger as t };
|
|
189
283
|
|
|
190
|
-
//# sourceMappingURL=logger-
|
|
284
|
+
//# sourceMappingURL=logger-DS03e908.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger-DS03e908.js","names":[],"sources":["../src/utils/host.ts","../src/utils/logger.ts"],"sourcesContent":["/**\n * The host globals this package reads, behind functions that do not assume Node.\n *\n * `process` is not defined on workerd, on Deno Deploy without the compat flag,\n * or in a browser. That matters here for one specific reason: the portable\n * authoring surface (`@rebasepro/server/functions`) reaches the logger and the\n * error handler, and a bare `process.env.NODE_ENV` inside either of them turns\n * the first log line of a request into a `ReferenceError` on a runtime that has\n * no `process` — a failure that reads as \"the framework crashed\" rather than\n * \"this runtime has no process object\".\n *\n * Nothing here throws and nothing here is async. A runtime that cannot answer\n * gets the empty answer, because every caller in this file's blast radius is\n * choosing a log level or a format, and the safe default for both is the\n * development one.\n *\n * @module\n */\n\n/**\n * Where an adapter with no `process` can publish the environment.\n *\n * Cloudflare Workers hand the environment to the *request*, not to the module,\n * so there is no global to read at import time. An edge adapter that has\n * already seen a request can stash the bag here and every contextless reader in\n * the framework — the logger, chiefly — starts answering correctly.\n *\n * `Symbol.for` rather than a module-local for the same reason the singleton\n * uses it: more than one copy of this module can be loaded into one process,\n * and a module-local would leave every copy but the writer's blind. See\n * `../singleton.ts`.\n *\n * Request-scoped code should NOT read this. Use `getEnv(c)` from\n * `@rebasepro/server/functions`, which reads the binding attached to the\n * request it is serving — the only correct source on a runtime where two\n * concurrent requests can carry different bindings.\n */\nconst ENV_SLOT = Symbol.for(\"@rebasepro/server:host-env\");\n\ntype GlobalWithEnv = typeof globalThis & {\n [ENV_SLOT]?: Record<string, string | undefined>;\n process?: { env?: Record<string, string | undefined> };\n};\n\n/**\n * The process environment, or the closest thing this runtime has to one.\n *\n * Order: a bag published by {@link setHostEnv} first, because an adapter that\n * set one knows more than the ambient globals do; then `process.env`; then\n * nothing.\n */\nexport function hostEnv(): Record<string, string | undefined> {\n const global = globalThis as GlobalWithEnv;\n return global[ENV_SLOT] ?? global.process?.env ?? {};\n}\n\n/**\n * Read one environment variable without touching `process` directly.\n *\n * Trimmed, and blank is treated as absent — a variable declared with no value\n * is the ordinary way to write a compose file or a `.env` line, and every\n * caller in this package means \"unset\" by it. See `resolveFunctionsTimeoutMs`,\n * which learned that the hard way.\n */\nexport function hostEnvVar(name: string): string | undefined {\n const raw = hostEnv()[name];\n if (typeof raw !== \"string\") return undefined;\n const trimmed = raw.trim();\n return trimmed === \"\" ? undefined : trimmed;\n}\n\n/**\n * Publish an environment bag for contextless readers.\n *\n * Called by an adapter for a runtime whose environment is not ambient. Merges\n * rather than replaces, so two adapters (or an adapter plus a test) do not\n * silently erase each other's variables.\n */\nexport function setHostEnv(env: Record<string, string | undefined>): void {\n const global = globalThis as GlobalWithEnv;\n global[ENV_SLOT] = {\n ...(global[ENV_SLOT] ?? {}),\n ...env\n };\n}\n\n/** @internal Test seam — drops anything {@link setHostEnv} published. */\nexport function _clearHostEnv(): void {\n delete (globalThis as GlobalWithEnv)[ENV_SLOT];\n}\n\ntype GlobalWithStdio = typeof globalThis & {\n process?: {\n stdout?: { write?: (chunk: string) => unknown };\n stderr?: { write?: (chunk: string) => unknown };\n };\n};\n\n/**\n * Write one already-formatted line to the process's output.\n *\n * `process.stdout.write` is preferred where it exists because it is the only\n * one of the two that does not append its own formatting to a line that is\n * already a complete JSON document — `console.log` on Node is\n * `process.stdout.write` plus `util.format`, and `util.format` will happily\n * reinterpret a `%s` that appeared inside a user's log message.\n *\n * Where it does not exist, `console` is the runtime's log sink and is what its\n * platform collects.\n */\nexport function writeLine(stream: \"out\" | \"err\", line: string): void {\n const proc = (globalThis as GlobalWithStdio).process;\n const sink = stream === \"err\" ? proc?.stderr : proc?.stdout;\n if (typeof sink?.write === \"function\") {\n sink.write(line + \"\\n\");\n return;\n }\n if (stream === \"err\") console.error(line);\n else console.log(line);\n}\n","/**\n * Structured Logger for Rebase Backend\n *\n * Outputs JSON lines when `NODE_ENV=production`, human-readable prefixed\n * lines otherwise. Designed to work with Google Cloud Logging severity levels.\n *\n * Every line — message and data, at any depth — passes through the redaction\n * below, which strips Drizzle's `Failed query: … / params: …` wrapper and the\n * values of secret-looking keys. See the block above `serialiseError`.\n *\n * Usage:\n * import { logger } from \"./utils/logger\";\n * logger.info(\"Server started\", { port: 3001 });\n * logger.error(\"Request failed\", { path: \"/api/test\", error: err });\n *\n * Every host global goes through `./host`, and that is load-bearing rather than\n * tidy: this module is reachable from `@rebasepro/server/functions`, the\n * authoring surface that has to import cleanly on a runtime with no `process`.\n * A bare `process.env.NODE_ENV` here would make the first log line of the first\n * request on workerd a `ReferenceError`.\n */\nimport { hostEnv, writeLine } from \"./host\";\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\n/** Google Cloud Logging severity strings. */\nconst GCP_SEVERITY: Record<LogLevel, string> = {\n debug: \"DEBUG\",\n info: \"INFO\",\n warn: \"WARNING\",\n error: \"ERROR\"\n};\n\nconst LOG_PRIORITY: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3\n};\n\nexport interface LogEntry {\n severity: string;\n message: string;\n timestamp: string;\n [key: string]: unknown;\n}\n\nexport interface Logger {\n debug(message: string, data?: Record<string, unknown>): void;\n info(message: string, data?: Record<string, unknown>): void;\n warn(message: string, data?: Record<string, unknown>): void;\n error(message: string, data?: Record<string, unknown>): void;\n child(defaultFields: Record<string, unknown>): Logger;\n}\n\nfunction isProduction(): boolean {\n return hostEnv().NODE_ENV === \"production\";\n}\n\nfunction getMinLevel(): LogLevel {\n const env = (hostEnv().LOG_LEVEL || \"info\").toLowerCase();\n if (env in LOG_PRIORITY) return env as LogLevel;\n return \"info\";\n}\n\n// ── Redaction ───────────────────────────────────────────────────────\n//\n// Drizzle builds every query failure as\n// `Failed query: ${query}\\nparams: ${params}` (drizzle-orm/errors.js), so the\n// statement *and* every bound value ride along in `.message` and `.stack` of\n// whatever a driver rethrows — an email and a bcrypt hash reach stdout the\n// moment a registration hits a unique violation. The redaction lives here, in\n// the one function every log line passes through, rather than at the ~124\n// `{ error: … }` call sites: a per-site rule is what produced the leak (one\n// file suppressed the stack, four others did not), and the next caller would\n// reintroduce it. Nothing above this line needs to know about it.\n\nconst FAILED_QUERY_MARKER = \"Failed query:\";\nconst REDACTED_QUERY = \"Failed query: [redacted]\";\nconst REDACTED_VALUE = \"[redacted]\";\n\n/**\n * Key fragments whose values are never safe to publish. Compared against the\n * key with separators and case removed, so `api_key`, `apiKey` and `API-KEY`\n * all match `apikey`.\n */\nconst SENSITIVE_KEY_FRAGMENTS = [\n \"password\",\n \"passwd\",\n \"passphrase\",\n \"secret\",\n \"token\",\n \"apikey\",\n \"authorization\",\n \"credential\",\n \"cookie\",\n \"privatekey\",\n \"sessionid\"\n];\n\n/** Longest structure the redactor will walk before giving up. */\nconst MAX_REDACT_DEPTH = 8;\n\nfunction isSensitiveKey(key: string): boolean {\n const normalised = key.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n return SENSITIVE_KEY_FRAGMENTS.some(fragment => normalised.includes(fragment));\n}\n\n/**\n * Local escape hatch for the `Failed query:` strip only — the statement is the\n * fastest way to diagnose a failing query on a developer machine. Ignored in\n * production, so a runtime that inherits the variable cannot leak because of\n * it, and it never re-enables the key deny-list.\n */\nfunction rawQueriesAllowed(): boolean {\n return hostEnv().NODE_ENV !== \"production\"\n && hostEnv().REBASE_LOG_RAW_QUERIES === \"true\";\n}\n\n/**\n * Strip every `Failed query: … / params: …` span out of a message or stack,\n * keeping the surrounding text (including stack frames, which carry no user\n * data). When no `params:` line follows the marker the rest of the string is\n * dropped: a statement of unknown extent is treated as sensitive rather than\n * guessed at.\n *\n * Idempotent, and it has to be: an already-redacted span still starts with the\n * marker but has no `params:` line, so a second pass over it would fall into\n * the drop-the-rest branch and eat the stack frames behind it. Redaction runs\n * more than once on the same string in practice — the cron scheduler redacts\n * before persisting and then logs the result.\n */\nexport function redactSensitiveText(text: string): string {\n if (!text.includes(FAILED_QUERY_MARKER) || rawQueriesAllowed()) return text;\n\n let out = text;\n let idx = out.indexOf(FAILED_QUERY_MARKER);\n while (idx !== -1) {\n if (out.startsWith(REDACTED_QUERY, idx)) {\n idx = out.indexOf(FAILED_QUERY_MARKER, idx + REDACTED_QUERY.length);\n continue;\n }\n const paramsIdx = out.indexOf(\"\\nparams:\", idx);\n let end: number;\n if (paramsIdx === -1) {\n end = out.length;\n } else {\n const eol = out.indexOf(\"\\n\", paramsIdx + 1);\n end = eol === -1 ? out.length : eol;\n }\n out = out.slice(0, idx) + REDACTED_QUERY + out.slice(end);\n idx = out.indexOf(FAILED_QUERY_MARKER, idx + REDACTED_QUERY.length);\n }\n return out;\n}\n\n/**\n * Serialise an Error into a plain object (stack + message), with the query\n * text redacted out of both. `query`/`params` own-properties — which\n * `DrizzleQueryError` carries beside the message — are deliberately not\n * copied.\n * Handles non-Error values gracefully.\n */\nfunction serialiseError(value: unknown): Record<string, unknown> {\n if (value instanceof Error) {\n return {\n name: value.name,\n message: redactSensitiveText(value.message),\n stack: value.stack ? redactSensitiveText(value.stack) : undefined\n };\n }\n return { value: redactSensitiveText(String(value)) };\n}\n\n/**\n * Redact one logged value: errors are serialised, strings are stripped of\n * query text, objects and arrays are walked. Cycles and over-deep structures\n * collapse to a marker rather than throwing — a logger that can fail is worse\n * than one that logs less. (An object referenced twice in one payload is\n * reported as `[circular]` the second time; bounding the walk matters more\n * than rendering a shared reference twice.)\n */\nfunction redactValue(value: unknown, depth: number, seen: WeakSet<object>): unknown {\n // `serialiseError` returns only already-redacted strings, so it is the\n // terminal step — walking its output again would redact twice.\n if (value instanceof Error) return serialiseError(value);\n if (typeof value === \"string\") return redactSensitiveText(value);\n if (value === null || typeof value !== \"object\") return value;\n if (depth >= MAX_REDACT_DEPTH) return \"[truncated]\";\n if (seen.has(value)) return \"[circular]\";\n seen.add(value);\n\n if (Array.isArray(value)) {\n return value.map(item => redactValue(item, depth + 1, seen));\n }\n if (value instanceof Date) return value;\n\n const out: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n out[key] = isSensitiveKey(key) ? REDACTED_VALUE : redactValue(val, depth + 1, seen);\n }\n return out;\n}\n\nfunction formatData(data?: Record<string, unknown>): Record<string, unknown> | undefined {\n if (!data) return undefined;\n const seen = new WeakSet<object>();\n const out: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(data)) {\n out[key] = isSensitiveKey(key) ? REDACTED_VALUE : redactValue(val, 0, seen);\n }\n return out;\n}\n\nfunction createLogger(rawDefaultFields: Record<string, unknown> = {}): Logger {\n const minLevel = getMinLevel();\n // Child fields go through the same pass as per-call data — they are merged\n // into every line this logger emits, so leaving them raw would be a hole\n // the moment `child()` gets its first caller.\n const defaultFields = formatData(rawDefaultFields) ?? {};\n\n function emit(level: LogLevel, message: string, data?: Record<string, unknown>): void {\n if (LOG_PRIORITY[level] < LOG_PRIORITY[minLevel]) return;\n\n // The message is redacted too, not just the data: several call sites\n // interpolate `error.message` straight into the line they log.\n const safeMessage = redactSensitiveText(message);\n const merged = { ...defaultFields,\n...formatData(data) };\n\n if (isProduction()) {\n // Structured JSON for Cloud Logging\n const entry: LogEntry = {\n severity: GCP_SEVERITY[level],\n message: safeMessage,\n timestamp: new Date().toISOString(),\n ...merged\n };\n const line = JSON.stringify(entry);\n\n if (level === \"error\") {\n writeLine(\"err\", line);\n } else {\n writeLine(\"out\", line);\n }\n } else {\n // Human-readable for development\n const prefix = level === \"error\" ? \"❌\"\n : level === \"warn\" ? \"⚠️\"\n : level === \"info\" ? \"ℹ️\"\n : \"🐛\";\n const extra = Object.keys(merged).length > 0 ? ` ${JSON.stringify(merged)}` : \"\";\n const out = `${prefix} [${level.toUpperCase()}] ${safeMessage}${extra}`;\n\n if (level === \"error\") {\n console.error(out);\n } else if (level === \"warn\") {\n console.warn(out);\n } else {\n console.log(out);\n }\n }\n }\n\n return {\n debug: (msg, data) => emit(\"debug\", msg, data),\n info: (msg, data) => emit(\"info\", msg, data),\n warn: (msg, data) => emit(\"warn\", msg, data),\n error: (msg, data) => emit(\"error\", msg, data),\n child(fields: Record<string, unknown>): Logger {\n return createLogger({ ...defaultFields,\n...fields });\n }\n };\n}\n\n/**\n * Singleton logger instance.\n * In production: emits JSON lines with `severity`, `message`, `timestamp`.\n * In development: emits human-readable prefixed lines.\n */\nexport const logger: Logger = createLogger();\n"],"mappings":"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"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { createRequire as
|
|
2
|
-
import "process";
|
|
3
|
-
|
|
4
|
-
import
|
|
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 { n as errorHandler, t as ApiError } from "./errors-DBwpj9N8.js";
|
|
5
7
|
import { Hono } from "hono";
|
|
6
8
|
import { streamSSE } from "hono/streaming";
|
|
7
9
|
//#region src/api/logs-routes.ts
|
|
@@ -147,25 +149,50 @@ var STREAM_HEARTBEAT_MS = 25e3;
|
|
|
147
149
|
* entries, a few MB, per stuck connection.
|
|
148
150
|
*/
|
|
149
151
|
var STREAM_MAX_PENDING = 2e3;
|
|
152
|
+
/**
|
|
153
|
+
* The largest window any of these routes will hand back.
|
|
154
|
+
*
|
|
155
|
+
* The ring buffer holds 10,000 entries, so asking for more than all of it is a
|
|
156
|
+
* mistake worth naming rather than silently clamping — and a clamped answer is
|
|
157
|
+
* indistinguishable from "that is all there is".
|
|
158
|
+
*/
|
|
159
|
+
var LOG_WINDOW_MAX = 1e4;
|
|
150
160
|
function createLogsRoutes(timing = {}) {
|
|
151
161
|
const flushMs = timing.flushMs ?? STREAM_FLUSH_MS;
|
|
152
162
|
const heartbeatMs = timing.heartbeatMs ?? STREAM_HEARTBEAT_MS;
|
|
153
163
|
const maxPending = timing.maxPending ?? STREAM_MAX_PENDING;
|
|
154
164
|
const app = new Hono();
|
|
165
|
+
app.onError(errorHandler);
|
|
166
|
+
/**
|
|
167
|
+
* A window into the ring buffer, or a 400 saying why not.
|
|
168
|
+
*
|
|
169
|
+
* `parseInt` was the whole of it before, and every malformed value failed
|
|
170
|
+
* differently and silently: `?limit=abc` fell through to the default,
|
|
171
|
+
* `?limit=-5` sliced an empty window and answered 200 with no entries, and
|
|
172
|
+
* `?count=abc` made `slice(-NaN)` return the *entire* buffer. Three ways to
|
|
173
|
+
* be wrong, none of them visible to the caller. The data plane refuses the
|
|
174
|
+
* same input with a 400 — see `resolveListLimitParam`.
|
|
175
|
+
*/
|
|
176
|
+
const window = (raw, what, max) => {
|
|
177
|
+
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
178
|
+
const parsed = Number(raw.trim());
|
|
179
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > max) throw new ApiError(400, "INVALID_PARAM", `Invalid \`${what}\`: ${raw}. Expected a whole number between 1 and ${max}.`, void 0, true);
|
|
180
|
+
return parsed;
|
|
181
|
+
};
|
|
155
182
|
app.get("/", (c) => {
|
|
156
183
|
const query = c.req.query();
|
|
157
184
|
const result = logBuffer.query({
|
|
158
185
|
level: query.level,
|
|
159
186
|
source: query.source,
|
|
160
187
|
search: query.search,
|
|
161
|
-
limit: query.limit
|
|
162
|
-
offset: query.offset
|
|
188
|
+
limit: window(query.limit, "limit", LOG_WINDOW_MAX),
|
|
189
|
+
offset: window(query.offset, "offset", Number.MAX_SAFE_INTEGER),
|
|
163
190
|
since: query.since
|
|
164
191
|
});
|
|
165
192
|
return c.json(result);
|
|
166
193
|
});
|
|
167
194
|
app.get("/latest", (c) => {
|
|
168
|
-
const count =
|
|
195
|
+
const count = window(c.req.query("count"), "count", LOG_WINDOW_MAX) ?? 50;
|
|
169
196
|
return c.json({ entries: logBuffer.getLatest(count) });
|
|
170
197
|
});
|
|
171
198
|
app.get("/stream", (c) => {
|
|
@@ -175,7 +202,7 @@ function createLogsRoutes(timing = {}) {
|
|
|
175
202
|
source: query.source,
|
|
176
203
|
search: query.search
|
|
177
204
|
});
|
|
178
|
-
const limit = query.limit
|
|
205
|
+
const limit = window(query.limit, "limit", LOG_WINDOW_MAX) ?? 200;
|
|
179
206
|
c.header("X-Accel-Buffering", "no");
|
|
180
207
|
return streamSSE(c, async (stream) => {
|
|
181
208
|
let pending = [];
|
|
@@ -242,4 +269,4 @@ var logs_routes_default = createLogsRoutes();
|
|
|
242
269
|
//#endregion
|
|
243
270
|
export { logs_routes_exports as n, logMiddleware as t };
|
|
244
271
|
|
|
245
|
-
//# sourceMappingURL=logs-routes-
|
|
272
|
+
//# sourceMappingURL=logs-routes-DB72iQSr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logs-routes-DB72iQSr.js","names":[],"sources":["../src/api/logs-routes.ts"],"sourcesContent":["import { Hono } from \"hono\";\nimport type { MiddlewareHandler } from \"hono\";\nimport { streamSSE } from \"hono/streaming\";\nimport type { HonoEnv } from \"./types\";\nimport { ApiError, errorHandler } from \"./errors\";\n\nexport interface LogEntry {\n id: string;\n timestamp: string;\n level: \"debug\" | \"info\" | \"warn\" | \"error\";\n source: \"api\" | \"auth\" | \"storage\" | \"realtime\" | \"system\";\n message: string;\n metadata?: Record<string, unknown>;\n}\n\n/** What a caller can narrow the log by, in either direction (query or stream). */\nexport interface LogFilterOptions {\n level?: string;\n source?: string;\n search?: string;\n since?: string;\n}\n\n/**\n * A filter with the search term already lowercased.\n *\n * The distinction matters on the stream path: `query()` lowercases once and then\n * scans, but a subscriber tests one entry at a time and would otherwise redo the\n * same `toLowerCase()` on every request the server handles.\n */\ntype NormalizedFilter = LogFilterOptions;\n\nfunction normalizeFilter(options: LogFilterOptions): NormalizedFilter {\n return { ...options,\n search: options.search?.toLowerCase() };\n}\n\n/**\n * Whether one entry belongs in a filtered view.\n *\n * Shared by the query and the stream on purpose: two copies of this would drift,\n * and the failure that produces is invisible — a tail that quietly shows a\n * different set of lines than the snapshot it started from.\n */\nfunction matchesFilter(entry: LogEntry, filter: NormalizedFilter): boolean {\n if (filter.level && entry.level !== filter.level) return false;\n if (filter.source && entry.source !== filter.source) return false;\n if (filter.search && !entry.message.toLowerCase().includes(filter.search)) return false;\n if (filter.since && entry.timestamp < filter.since) return false;\n return true;\n}\n\n/** Notified for every entry pushed, in push order. */\nexport type LogListener = (entry: LogEntry) => void;\n\nclass LogRingBuffer {\n private buffer: LogEntry[] = [];\n private maxSize: number;\n private idCounter = 0;\n private listeners = new Set<LogListener>();\n\n constructor(maxSize = 10000) {\n this.maxSize = maxSize;\n }\n\n push(entry: Omit<LogEntry, \"id\">): void {\n const id = `log_${++this.idCounter}`;\n const stored: LogEntry = { ...entry,\n id };\n this.buffer.push(stored);\n if (this.buffer.length > this.maxSize) {\n this.buffer.shift();\n }\n // This runs on the request hot path, so a listener must never be able to\n // take the request down with it: a tail that throws loses its own tail,\n // not the response the log line was describing.\n for (const listener of this.listeners) {\n try {\n listener(stored);\n } catch {\n /* a broken tail is not the request's problem */\n }\n }\n }\n\n /**\n * Follow the buffer. Returns the unsubscribe — call it, always: a listener\n * left behind holds its whole closure, and on this class that closure is a\n * pending-entry array.\n *\n * A listener must not log. It is called from inside `push`, so anything that\n * reaches `addLog` from here recurses until the stack gives out.\n */\n subscribe(listener: LogListener): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n query(options: LogFilterOptions & {\n limit?: number;\n offset?: number;\n }): { entries: LogEntry[]; total: number } {\n const filter = normalizeFilter(options);\n const filtered = this.buffer.filter(e => matchesFilter(e, filter));\n\n // Newest first\n const sorted = [...filtered].reverse();\n const total = sorted.length;\n const limit = options.limit || 100;\n const offset = options.offset || 0;\n\n return {\n entries: sorted.slice(offset, offset + limit),\n total\n };\n }\n\n getLatest(count = 50): LogEntry[] {\n return this.buffer.slice(-count).reverse();\n }\n}\n\n// Global singleton\nexport const logBuffer = new LogRingBuffer();\n\n/** Add a log entry */\nexport function addLog(\n level: LogEntry[\"level\"],\n source: LogEntry[\"source\"],\n message: string,\n metadata?: Record<string, unknown>\n): void {\n logBuffer.push({\n timestamp: new Date().toISOString(),\n level,\n source,\n message,\n metadata\n });\n}\n\nexport interface LogMiddlewareOptions {\n /**\n * Paths this sink ignores, matched exactly against `c.req.path`.\n *\n * For requests whose only reason to exist is to read the log. Recording\n * those makes the reader the loudest thing in its own output, and on a quiet\n * server it is also the thing evicting real entries out of the ring.\n */\n ignorePaths?: string[];\n}\n\n/** Hono middleware to log API requests */\nexport function logMiddleware(options: LogMiddlewareOptions = {}): MiddlewareHandler<HonoEnv> {\n const ignored = new Set(options.ignorePaths ?? []);\n return async (c, next) => {\n const start = Date.now();\n await next();\n if (ignored.has(c.req.path)) return;\n const duration = Date.now() - start;\n const reqId = c.get(\"requestId\");\n addLog(\"info\", \"api\", `${c.req.method} ${c.req.path} ${c.res.status} ${duration}ms`, {\n method: c.req.method,\n path: c.req.path,\n status: c.res.status,\n duration,\n ...(reqId && { requestId: reqId })\n });\n };\n}\n\n/**\n * How long entries accumulate before a batch goes out.\n *\n * Not zero, and that is the point. A busy server logs faster than a browser can\n * render, and one SSE frame per line would hand the client a re-render per\n * request served — worse than the 3s poll this replaces, precisely when the logs\n * are worth watching. Coalescing keeps the frame rate bounded by the window\n * rather than by traffic, and 250ms still reads as \"live\" to a person.\n */\nconst STREAM_FLUSH_MS = 250;\n\n/**\n * Idle gap after which the stream sends a comment line.\n *\n * A silent SSE connection is indistinguishable from a dead one to everything in\n * between — proxies, load balancers and laptop NICs all reap idle sockets, and a\n * server with nothing to say is the normal state here.\n */\nconst STREAM_HEARTBEAT_MS = 25_000;\n\n/**\n * Entries a single connection will hold between flushes.\n *\n * This is a *rate* ceiling, not just a memory bound, and that is easy to get\n * wrong: nothing drains `pending` between flushes, so the most a connection can\n * carry losslessly is `maxPending` per `flushMs` — here 2000 per 250ms, or 8000\n * entries a second. Above that the oldest pending entries go and the client is\n * told how many, whatever speed it is reading at.\n *\n * It was 500, which put that ceiling at 2000/s. A healthy reader on a loopback\n * socket lost 85% of a 20k burst to it — the cap fired on the server's own\n * coalescing window rather than on any slowness at the client, which is a drop\n * notice that says nothing true about why. 8000/s is past what one Node process\n * serves, so reaching it now means genuinely more log than a person can be shown.\n *\n * The memory this bounds is the copy a *stalled* reader causes: roughly 2000\n * entries, a few MB, per stuck connection.\n */\nconst STREAM_MAX_PENDING = 2000;\n\n/**\n * The largest window any of these routes will hand back.\n *\n * The ring buffer holds 10,000 entries, so asking for more than all of it is a\n * mistake worth naming rather than silently clamping — and a clamped answer is\n * indistinguishable from \"that is all there is\".\n */\nconst LOG_WINDOW_MAX = 10_000;\n\n/**\n * The stream's timings, injectable only so they can be tested.\n *\n * The defaults above are the contract and nothing in production passes this. A\n * heartbeat is a 25-second wait to observe, and a suite that cannot observe it is\n * a suite where the keepalive can rot — which surfaces as \"the tail dies after a\n * few minutes behind the load balancer\", months later, on someone else's cluster.\n */\nexport interface LogStreamTiming {\n flushMs?: number;\n heartbeatMs?: number;\n maxPending?: number;\n}\n\nexport function createLogsRoutes(timing: LogStreamTiming = {}): Hono<HonoEnv> {\n const flushMs = timing.flushMs ?? STREAM_FLUSH_MS;\n const heartbeatMs = timing.heartbeatMs ?? STREAM_HEARTBEAT_MS;\n const maxPending = timing.maxPending ?? STREAM_MAX_PENDING;\n\n const app = new Hono<HonoEnv>();\n // Its own, like every other router here: nothing registers one on the host\n // app — not `boot.ts`, not the scaffolded backend, not the eject template —\n // so a router that throws without this answers Hono's default 500 in plain\n // text, outside the `{ error: { code, message } }` envelope the rest of the\n // API keeps to.\n app.onError(errorHandler);\n\n /**\n * A window into the ring buffer, or a 400 saying why not.\n *\n * `parseInt` was the whole of it before, and every malformed value failed\n * differently and silently: `?limit=abc` fell through to the default,\n * `?limit=-5` sliced an empty window and answered 200 with no entries, and\n * `?count=abc` made `slice(-NaN)` return the *entire* buffer. Three ways to\n * be wrong, none of them visible to the caller. The data plane refuses the\n * same input with a 400 — see `resolveListLimitParam`.\n */\n const window = (raw: string | undefined, what: string, max: number): number | undefined => {\n if (raw === undefined || raw.trim() === \"\") return undefined;\n const parsed = Number(raw.trim());\n if (!Number.isInteger(parsed) || parsed < 1 || parsed > max) {\n throw new ApiError(\n 400,\n \"INVALID_PARAM\",\n `Invalid \\`${what}\\`: ${raw}. Expected a whole number between 1 and ${max}.`,\n undefined,\n true\n );\n }\n return parsed;\n };\n\n // GET /api/logs — Query logs\n app.get(\"/\", (c) => {\n const query = c.req.query();\n const result = logBuffer.query({\n level: query.level,\n source: query.source,\n search: query.search,\n limit: window(query.limit, \"limit\", LOG_WINDOW_MAX),\n offset: window(query.offset, \"offset\", Number.MAX_SAFE_INTEGER),\n since: query.since\n });\n return c.json(result);\n });\n\n // GET /api/logs/latest — Get latest logs (for real-time)\n app.get(\"/latest\", (c) => {\n const count = window(c.req.query(\"count\"), \"count\", LOG_WINDOW_MAX) ?? 50;\n return c.json({ entries: logBuffer.getLatest(count) });\n });\n\n // GET /api/logs/stream — tail the buffer over SSE.\n //\n // The Logs Explorer used to poll this router every 3 seconds, which cost a\n // request per client per 3s to say \"nothing happened\" and still showed each\n // line up to 3s late. Here the buffer pushes instead, so an idle server is an\n // idle socket.\n //\n // Events:\n // snapshot {entries, total} the filtered window, oldest-first, at open\n // append {entries, dropped} entries since the last frame, oldest-first\n // `: ping` comment, keepalive only\n //\n // Snapshot and appends come down the same connection deliberately. A client\n // that fetched its backlog separately would race the subscription — entries\n // logged between the two calls belong to neither — and closing that race from\n // the outside needs an id cursor and dedupe on every frame.\n app.get(\"/stream\", (c) => {\n const query = c.req.query();\n const filter = normalizeFilter({\n level: query.level,\n source: query.source,\n search: query.search\n });\n const limit = window(query.limit, \"limit\", LOG_WINDOW_MAX) ?? 200;\n\n // Reverse proxies buffer text responses by default, which turns a live\n // tail into nothing at all until the buffer fills. nginx (and the ingress\n // in front of the managed runtime) reads this header; everything else\n // ignores it. The rest of the SSE headers are set by `streamSSE`.\n c.header(\"X-Accel-Buffering\", \"no\");\n\n return streamSSE(c, async (stream) => {\n let pending: LogEntry[] = [];\n let dropped = 0;\n\n // Subscribe *before* reading the backlog, with nothing awaited\n // between the two. Both are synchronous, so the two halves meet\n // exactly: an entry logged after the query but before the\n // subscription would otherwise be in neither, and that gap is the one\n // thing this route exists to close.\n const unsubscribe = logBuffer.subscribe(entry => {\n if (!matchesFilter(entry, filter)) return;\n if (pending.length >= maxPending) {\n pending.shift();\n dropped++;\n }\n pending.push(entry);\n });\n const snapshot = logBuffer.query({ ...filter,\n limit });\n\n // A client that goes away has to end this handler, or the\n // subscription outlives the socket. `streamSSE` only wires the\n // request signal through on old Bun, so do it here and let\n // `stream.aborted` be the one condition the loop tests.\n //\n // The `aborted` check is not belt-and-braces. A listener added to an\n // already-aborted signal is never called, so a client that leaves\n // during the snapshot write — a fast navigation, or a reconnect storm\n // against a restarting server — would leave this handler with no way\n // to learn it had gone: a subscriber and a flush loop, per attempt,\n // for the life of the process.\n const abortOnDisconnect = () => {\n if (!stream.closed) stream.abort();\n };\n c.req.raw.signal.addEventListener(\"abort\", abortOnDisconnect, { once: true });\n if (c.req.raw.signal.aborted) abortOnDisconnect();\n\n try {\n await stream.writeSSE({\n event: \"snapshot\",\n // The view tails like a terminal; both frames are oldest-first\n // so the client only ever appends.\n data: JSON.stringify({\n entries: snapshot.entries.slice().reverse(),\n total: snapshot.total\n })\n });\n\n let idleMs = 0;\n while (!stream.aborted && !stream.closed) {\n await stream.sleep(flushMs);\n if (stream.aborted || stream.closed) break;\n\n if (pending.length === 0) {\n idleMs += flushMs;\n if (idleMs >= heartbeatMs) {\n await stream.write(\": ping\\n\\n\");\n idleMs = 0;\n }\n continue;\n }\n\n const entries = pending;\n const lost = dropped;\n pending = [];\n dropped = 0;\n idleMs = 0;\n await stream.writeSSE({\n event: \"append\",\n data: JSON.stringify(lost > 0 ? { entries,\n dropped: lost } : { entries })\n });\n }\n } finally {\n unsubscribe();\n c.req.raw.signal.removeEventListener(\"abort\", abortOnDisconnect);\n }\n });\n });\n\n return app;\n}\n\nexport default createLogsRoutes();\n"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DataDriver } from "@rebasepro/types";
|
|
2
|
+
import { type MetricSeries, type SeriesPoint } from "./history-store.js";
|
|
3
|
+
export interface MetricsHistory {
|
|
4
|
+
/** Create the table and sweep what has aged out. */
|
|
5
|
+
ensure(): Promise<void>;
|
|
6
|
+
/** Begin sampling this process. Returns a stop. */
|
|
7
|
+
start(): () => void;
|
|
8
|
+
/** Read one series, for the route and for anything else that asks. */
|
|
9
|
+
read(series: MetricSeries, sinceMinutes: number): Promise<SeriesPoint[]>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* History for this deployment, or `undefined` when it cannot have any.
|
|
13
|
+
*
|
|
14
|
+
* Undefined rather than a no-op: the route turns it into a named 501, because a
|
|
15
|
+
* chart that renders empty is indistinguishable from a quiet period and this is
|
|
16
|
+
* exactly the class of silence the panel it feeds exists to remove.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createMetricsHistory(driver: DataDriver): MetricsHistory | undefined;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export type Exec = (sql: string, params?: unknown[]) => Promise<unknown>;
|
|
2
|
+
/** Where the samples live. Framework-owned, like `rebase.jobs`. */
|
|
3
|
+
export declare const METRICS_HISTORY_TABLE = "rebase.metric_samples";
|
|
4
|
+
/**
|
|
5
|
+
* How long a sample is kept.
|
|
6
|
+
*
|
|
7
|
+
* Two weeks answers "is this worse than last week" and stops well short of
|
|
8
|
+
* being an archive. Anything that needs to outlive it — billing, capacity
|
|
9
|
+
* planning — is a rollup somebody else owns, not a longer retention here.
|
|
10
|
+
*
|
|
11
|
+
* Read it as "14 days, or since this process started, whichever is longer": the
|
|
12
|
+
* sweep runs at boot and nowhere else, so a pod up for 90 days holds 90 days.
|
|
13
|
+
* That is a deliberate trade rather than an oversight — a cron for it would be
|
|
14
|
+
* machinery for a table that stays trivial either way, and reads are bounded by
|
|
15
|
+
* the index regardless — but the table does not "reach a steady size and stay
|
|
16
|
+
* there" on a long-lived pod, which an earlier version of this comment claimed.
|
|
17
|
+
*/
|
|
18
|
+
export declare const RETENTION_DAYS = 14;
|
|
19
|
+
/** How often the process samples itself. Matched to the resolution it stores. */
|
|
20
|
+
export declare const SAMPLE_INTERVAL_MS = 60000;
|
|
21
|
+
/**
|
|
22
|
+
* The series this records. A closed set on purpose — see the cardinality note.
|
|
23
|
+
*
|
|
24
|
+
* A list rather than only a union, because the route validates against it: an
|
|
25
|
+
* unknown `?series=` must be a named 400 rather than an empty chart, which
|
|
26
|
+
* reads exactly like a quiet period.
|
|
27
|
+
*
|
|
28
|
+
* **It lists what `sampleSelf` actually writes, and nothing else.** It used to
|
|
29
|
+
* also name `requests_total`, `errors_total` and `event_loop_delay_ms`, none of
|
|
30
|
+
* which anything ever recorded — so those three were *valid* parameters that
|
|
31
|
+
* returned `points: []`, which is precisely the empty chart the 400 two
|
|
32
|
+
* paragraphs up exists to prevent. A declared-but-unwritten series is worse
|
|
33
|
+
* than an absent one: the 400 tells you the name is wrong, and the empty array
|
|
34
|
+
* tells you the app was quiet.
|
|
35
|
+
*
|
|
36
|
+
* `event_loop_delay_ms` kept its place by gaining a sampler, because it is the
|
|
37
|
+
* one signal here that says whether the process is *healthy* rather than how
|
|
38
|
+
* much it is using, and it has no counter semantics to get wrong.
|
|
39
|
+
*
|
|
40
|
+
* `requests_total` and `errors_total` were dropped rather than wired up. The
|
|
41
|
+
* registry does hold them, but they reset on restart, and a resetting counter
|
|
42
|
+
* summed across replicas is not monotonic — a rolling deploy would draw a
|
|
43
|
+
* cliff that looks like traffic collapsing. That needs a deliberate decision
|
|
44
|
+
* about deltas and resets, not a line added at 2am. Adding either back means
|
|
45
|
+
* adding its sampler in the same commit.
|
|
46
|
+
*/
|
|
47
|
+
export declare const METRIC_SERIES: readonly ["cpu_millicores", "memory_bytes", "event_loop_delay_ms"];
|
|
48
|
+
export type MetricSeries = (typeof METRIC_SERIES)[number];
|
|
49
|
+
export interface MetricSample {
|
|
50
|
+
at: Date;
|
|
51
|
+
series: MetricSeries;
|
|
52
|
+
value: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create the table and sweep what has aged out.
|
|
56
|
+
*
|
|
57
|
+
* Called at boot, beside the job and cron stores, and for the same reason they
|
|
58
|
+
* do it there: the only moment the schema is guaranteed to be reachable and
|
|
59
|
+
* nobody is mid-request.
|
|
60
|
+
*/
|
|
61
|
+
export declare function ensureMetricsHistory(exec: Exec): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* What this process is using right now.
|
|
64
|
+
*
|
|
65
|
+
* `process.cpuUsage()` is cumulative, so a rate needs two readings and the gap
|
|
66
|
+
* between them — which is why the previous one is threaded through rather than
|
|
67
|
+
* held in a module global: a module global is shared by every test in a file
|
|
68
|
+
* and makes the first assertion depend on whatever ran before it.
|
|
69
|
+
*/
|
|
70
|
+
export declare function sampleSelf(previous: {
|
|
71
|
+
cpu: NodeJS.CpuUsage;
|
|
72
|
+
at: number;
|
|
73
|
+
} | null, now?: number,
|
|
74
|
+
/**
|
|
75
|
+
* Mean event-loop delay over the last window, in milliseconds, or undefined
|
|
76
|
+
* where it cannot be measured. Passed in rather than read here so this
|
|
77
|
+
* function stays pure: the histogram is a stateful handle the recorder owns.
|
|
78
|
+
*/
|
|
79
|
+
eventLoopDelayMs?: number): {
|
|
80
|
+
samples: Omit<MetricSample, "at">[];
|
|
81
|
+
cursor: {
|
|
82
|
+
cpu: NodeJS.CpuUsage;
|
|
83
|
+
at: number;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Write one tick's samples, for one instance.
|
|
88
|
+
*
|
|
89
|
+
* ## Why `instance` is part of the key
|
|
90
|
+
*
|
|
91
|
+
* A tenant's replicas share one database, and each records its OWN process. Key
|
|
92
|
+
* a row by `(series, minute)` alone and the pods overwrite each other every
|
|
93
|
+
* tick: one pod at 5m and another at 500m leave whichever wrote last, so a
|
|
94
|
+
* scaled-out tenant charts one arbitrary replica and calls it the app. Adding
|
|
95
|
+
* the instance makes each pod its own row, and lets the read decide whether the
|
|
96
|
+
* question is "the whole deployment" or "which pod is hot".
|
|
97
|
+
*
|
|
98
|
+
* Cardinality stays bounded: rows are series × replicas × minutes, and replicas
|
|
99
|
+
* are capped by the autoscaling ceiling. Six pods is ~43k rows a day and a
|
|
100
|
+
* fortnight of them is well under a million.
|
|
101
|
+
*/
|
|
102
|
+
export declare function recordSamples(exec: Exec, samples: Omit<MetricSample, "at">[], instance: string, at?: Date): Promise<void>;
|
|
103
|
+
export interface SeriesPoint {
|
|
104
|
+
at: string;
|
|
105
|
+
/** The deployment's figure, combined per `COMBINE`. */
|
|
106
|
+
value: number;
|
|
107
|
+
/** How many instances reported in this bucket. */
|
|
108
|
+
instances: number;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Read one series over a window, oldest first — the order a chart draws in.
|
|
112
|
+
*
|
|
113
|
+
* Combined across instances rather than returned per-pod. A chart of "this
|
|
114
|
+
* app's CPU" is the question people ask; "which pod is hot" is answered by the
|
|
115
|
+
* live panel, which already lists instances individually and does not need
|
|
116
|
+
* history to do it.
|
|
117
|
+
*
|
|
118
|
+
* `instances` rides along because a sum whose contributor count changed is not
|
|
119
|
+
* comparable with itself: CPU doubling because the app got busy and CPU
|
|
120
|
+
* doubling because it scaled from one replica to two are different events, and
|
|
121
|
+
* a line chart alone cannot tell them apart.
|
|
122
|
+
*/
|
|
123
|
+
export declare function readSeries(exec: Exec, series: MetricSeries, sinceMinutes: number): Promise<SeriesPoint[]>;
|
package/dist/metrics/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import type { MiddlewareHandler } from "hono";
|
|
3
|
-
import type
|
|
3
|
+
import { type MetricSeries, type SeriesPoint } from "./history-store.js";
|
|
4
|
+
import type { HonoEnv } from "../api/types.js";
|
|
4
5
|
/**
|
|
5
6
|
* Runtime metrics, in Prometheus text format.
|
|
6
7
|
*
|
|
@@ -80,4 +81,11 @@ export declare function createMetricsMiddleware(basePath?: string): MetricsHandl
|
|
|
80
81
|
* When a token is configured it is required, and compared in constant time — a
|
|
81
82
|
* timing oracle on a metrics token is a small thing, but it is free to avoid.
|
|
82
83
|
*/
|
|
83
|
-
export declare function createMetricsRoutes(registry: MetricsRegistry, token?: string
|
|
84
|
+
export declare function createMetricsRoutes(registry: MetricsRegistry, token?: string,
|
|
85
|
+
/**
|
|
86
|
+
* Reads a stored series. Absent when the deployment keeps no history — a
|
|
87
|
+
* driver with no SQL admin, or a role that does not provision — and the
|
|
88
|
+
* route then answers 501 rather than pretending an empty chart is a quiet
|
|
89
|
+
* period.
|
|
90
|
+
*/
|
|
91
|
+
history?: (series: MetricSeries, sinceMinutes: number) => Promise<SeriesPoint[]>): Hono<HonoEnv>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { createRequire as
|
|
2
|
-
import "process";
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import "./src-
|
|
1
|
+
import { createRequire as __rebaseCreateRequire } from "module";
|
|
2
|
+
import __rebaseProcess from "process";
|
|
3
|
+
globalThis.process ??= __rebaseProcess;
|
|
4
|
+
__rebaseCreateRequire(import.meta.url);
|
|
5
|
+
import { y as resolveCollectionRelations } from "./src-B-CmIFMr.js";
|
|
6
|
+
import "./src-Cdsw7DqV.js";
|
|
6
7
|
//#region ../types/src/types/relations.ts
|
|
7
8
|
/** @group Models */
|
|
8
9
|
function isToMany(relation) {
|
|
@@ -503,11 +504,6 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
503
504
|
}
|
|
504
505
|
},
|
|
505
506
|
patch: updateOperation(collection, schemaName, requireAuth),
|
|
506
|
-
put: {
|
|
507
|
-
...updateOperation(collection, schemaName, requireAuth),
|
|
508
|
-
operationId: `update${schemaName}ViaPut`,
|
|
509
|
-
deprecated: true
|
|
510
|
-
},
|
|
511
507
|
delete: {
|
|
512
508
|
tags: [collection.name],
|
|
513
509
|
summary: `Delete ${collection.singularName || collection.name}`,
|
|
@@ -927,4 +923,4 @@ function toPascalCase(str) {
|
|
|
927
923
|
//#endregion
|
|
928
924
|
export { generateOpenApiSpec };
|
|
929
925
|
|
|
930
|
-
//# sourceMappingURL=openapi-generator-
|
|
926
|
+
//# sourceMappingURL=openapi-generator-FFGT0H6O.js.map
|