@rebasepro/server 0.16.0 → 0.16.1-canary.g2d1aec8
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/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 +111 -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 +3 -4
- package/dist/{schema-editor-routes-CV9k0w3G.js → ast-schema-editor-BpFYpT38.js} +2 -39
- package/dist/ast-schema-editor-BpFYpT38.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 -4
- 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 +3 -3
- package/dist/auth/bitbucket-oauth.d.ts +2 -2
- package/dist/auth/builtin-auth-adapter.d.ts +7 -4
- 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 +56 -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 +5 -5
- 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 +13 -4
- 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-C74dqR2B.js} +250 -15
- package/dist/auth-C74dqR2B.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-BJ86ah4T.js} +2 -2
- package/dist/{backup-C6ljYVTp.js.map → backup-BJ86ah4T.js.map} +1 -1
- package/dist/boot/boot.d.ts +5 -5
- package/dist/boot/bundle.d.ts +1 -1
- package/dist/boot/driver.d.ts +1 -1
- package/dist/boot/env.d.ts +13 -1
- package/dist/boot/options.d.ts +30 -6
- package/dist/boot/role.d.ts +3 -2
- package/dist/boot/sources.d.ts +1 -1
- package/dist/collections/BackendCollectionRegistry.d.ts +1 -1
- package/dist/collections/loader.d.ts +1 -1
- package/dist/{contract-routes-DZ-LBpSL.js → contract-routes-BEq7euZg.js} +2 -2
- package/dist/contract-routes-BEq7euZg.js.map +1 -0
- package/dist/cron/cron-routes.d.ts +2 -2
- package/dist/cron/cron-scheduler.d.ts +2 -2
- package/dist/cron/index.d.ts +8 -8
- package/dist/{cron-loader-YhhQeVBM.js → cron-loader-BMvtW6-J.js} +2 -2
- package/dist/{cron-loader-YhhQeVBM.js.map → cron-loader-BMvtW6-J.js.map} +1 -1
- package/dist/{cron-routes-maM_RlUu.js → cron-routes-BvYk-Kmi.js} +2 -2
- package/dist/{cron-routes-maM_RlUu.js.map → cron-routes-BvYk-Kmi.js.map} +1 -1
- package/dist/{cron-scheduler-DIpYBmZP.js → cron-scheduler-Buf-uVam.js} +2 -2
- package/dist/{cron-scheduler-DIpYBmZP.js.map → cron-scheduler-Buf-uVam.js.map} +1 -1
- package/dist/{cron-store-DfH_4Cd9.js → cron-store-CyF-oInV.js} +3 -3
- package/dist/{cron-store-DfH_4Cd9.js.map → cron-store-CyF-oInV.js.map} +1 -1
- package/dist/{ddl-bootstrap-Cywoj8Ta.js → ddl-bootstrap-DgSnERYN.js} +17 -3
- package/dist/ddl-bootstrap-DgSnERYN.js.map +1 -0
- package/dist/dev-secrets.d.ts +52 -0
- package/dist/email/dev-sink.d.ts +67 -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 +1 -1
- package/dist/env.d.ts +1 -1
- package/dist/{errors-EBYiaJ2E.js → errors-D_LwNKRM.js} +5 -5
- package/dist/errors-D_LwNKRM.js.map +1 -0
- package/dist/{function-loader-DDS1v7YX.js → function-loader-BrLmC_-y.js} +3 -3
- package/dist/{function-loader-DDS1v7YX.js.map → function-loader-BrLmC_-y.js.map} +1 -1
- package/dist/functions/context.d.ts +141 -0
- package/dist/functions/define-function.d.ts +1 -1
- package/dist/functions/function-routes.d.ts +2 -2
- 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/index.d.ts +63 -54
- package/dist/index.es.js +2183 -164
- 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 +104 -18
- package/dist/jobs/index.d.ts +5 -5
- package/dist/{jobs-CyOKXXlu.js → jobs-Bn6nhY8J.js} +3 -3
- package/dist/{jobs-CyOKXXlu.js.map → jobs-Bn6nhY8J.js.map} +1 -1
- package/dist/{jwt-DxH9fLPt.js → jwt-BbJi0TR0.js} +2 -2
- package/dist/{jwt-DxH9fLPt.js.map → jwt-BbJi0TR0.js.map} +1 -1
- package/dist/{logger-DfvF_8r-.js → logger-TdvXIGqR.js} +101 -8
- package/dist/logger-TdvXIGqR.js.map +1 -0
- package/dist/metrics/index.d.ts +1 -1
- package/dist/{proxy-Bj5DVllb.js → proxy-QJKSS-CV.js} +5 -3
- package/dist/{proxy-Bj5DVllb.js.map → proxy-QJKSS-CV.js.map} +1 -1
- package/dist/{request-timeout-BuFoEKwT.js → request-timeout-OofPCHQT.js} +17 -3
- package/dist/request-timeout-OofPCHQT.js.map +1 -0
- package/dist/rls-audit/index.d.ts +111 -0
- 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 +29 -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-yh2cQcdJ.js +44 -0
- package/dist/schema-editor-routes-yh2cQcdJ.js.map +1 -0
- package/dist/services/webhook-service.d.ts +1 -1
- package/dist/singleton.d.ts +7 -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/policies.d.ts +88 -0
- package/dist/storage/range.d.ts +63 -0
- package/dist/storage/routes.d.ts +3 -3
- package/dist/storage/storage-registry.d.ts +1 -1
- package/dist/storage/tus-handler.d.ts +2 -2
- 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/auth-5Et5mnUA.js.map +0 -1
- package/dist/contract-routes-DZ-LBpSL.js.map +0 -1
- package/dist/ddl-bootstrap-Cywoj8Ta.js.map +0 -1
- package/dist/errors-EBYiaJ2E.js.map +0 -1
- package/dist/logger-DfvF_8r-.js.map +0 -1
- package/dist/request-timeout-BuFoEKwT.js.map +0 -1
- package/dist/schema-editor-routes-CV9k0w3G.js.map +0 -1
|
@@ -1,7 +1,100 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
|
-
import
|
|
2
|
+
import "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
|
+
//#region src/utils/host.ts
|
|
5
|
+
/**
|
|
6
|
+
* The host globals this package reads, behind functions that do not assume Node.
|
|
7
|
+
*
|
|
8
|
+
* `process` is not defined on workerd, on Deno Deploy without the compat flag,
|
|
9
|
+
* or in a browser. That matters here for one specific reason: the portable
|
|
10
|
+
* authoring surface (`@rebasepro/server/functions`) reaches the logger and the
|
|
11
|
+
* error handler, and a bare `process.env.NODE_ENV` inside either of them turns
|
|
12
|
+
* the first log line of a request into a `ReferenceError` on a runtime that has
|
|
13
|
+
* no `process` — a failure that reads as "the framework crashed" rather than
|
|
14
|
+
* "this runtime has no process object".
|
|
15
|
+
*
|
|
16
|
+
* Nothing here throws and nothing here is async. A runtime that cannot answer
|
|
17
|
+
* gets the empty answer, because every caller in this file's blast radius is
|
|
18
|
+
* choosing a log level or a format, and the safe default for both is the
|
|
19
|
+
* development one.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Where an adapter with no `process` can publish the environment.
|
|
25
|
+
*
|
|
26
|
+
* Cloudflare Workers hand the environment to the *request*, not to the module,
|
|
27
|
+
* so there is no global to read at import time. An edge adapter that has
|
|
28
|
+
* already seen a request can stash the bag here and every contextless reader in
|
|
29
|
+
* the framework — the logger, chiefly — starts answering correctly.
|
|
30
|
+
*
|
|
31
|
+
* `Symbol.for` rather than a module-local for the same reason the singleton
|
|
32
|
+
* uses it: more than one copy of this module can be loaded into one process,
|
|
33
|
+
* and a module-local would leave every copy but the writer's blind. See
|
|
34
|
+
* `../singleton.ts`.
|
|
35
|
+
*
|
|
36
|
+
* Request-scoped code should NOT read this. Use `getEnv(c)` from
|
|
37
|
+
* `@rebasepro/server/functions`, which reads the binding attached to the
|
|
38
|
+
* request it is serving — the only correct source on a runtime where two
|
|
39
|
+
* concurrent requests can carry different bindings.
|
|
40
|
+
*/
|
|
41
|
+
var ENV_SLOT = Symbol.for("@rebasepro/server:host-env");
|
|
42
|
+
/**
|
|
43
|
+
* The process environment, or the closest thing this runtime has to one.
|
|
44
|
+
*
|
|
45
|
+
* Order: a bag published by {@link setHostEnv} first, because an adapter that
|
|
46
|
+
* set one knows more than the ambient globals do; then `process.env`; then
|
|
47
|
+
* nothing.
|
|
48
|
+
*/
|
|
49
|
+
function hostEnv() {
|
|
50
|
+
const global = globalThis;
|
|
51
|
+
return global[ENV_SLOT] ?? global.process?.env ?? {};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Write one already-formatted line to the process's output.
|
|
55
|
+
*
|
|
56
|
+
* `process.stdout.write` is preferred where it exists because it is the only
|
|
57
|
+
* one of the two that does not append its own formatting to a line that is
|
|
58
|
+
* already a complete JSON document — `console.log` on Node is
|
|
59
|
+
* `process.stdout.write` plus `util.format`, and `util.format` will happily
|
|
60
|
+
* reinterpret a `%s` that appeared inside a user's log message.
|
|
61
|
+
*
|
|
62
|
+
* Where it does not exist, `console` is the runtime's log sink and is what its
|
|
63
|
+
* platform collects.
|
|
64
|
+
*/
|
|
65
|
+
function writeLine(stream, line) {
|
|
66
|
+
const proc = globalThis.process;
|
|
67
|
+
const sink = stream === "err" ? proc?.stderr : proc?.stdout;
|
|
68
|
+
if (typeof sink?.write === "function") {
|
|
69
|
+
sink.write(line + "\n");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (stream === "err") console.error(line);
|
|
73
|
+
else console.log(line);
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
4
76
|
//#region src/utils/logger.ts
|
|
77
|
+
/**
|
|
78
|
+
* Structured Logger for Rebase Backend
|
|
79
|
+
*
|
|
80
|
+
* Outputs JSON lines when `NODE_ENV=production`, human-readable prefixed
|
|
81
|
+
* lines otherwise. Designed to work with Google Cloud Logging severity levels.
|
|
82
|
+
*
|
|
83
|
+
* Every line — message and data, at any depth — passes through the redaction
|
|
84
|
+
* below, which strips Drizzle's `Failed query: … / params: …` wrapper and the
|
|
85
|
+
* values of secret-looking keys. See the block above `serialiseError`.
|
|
86
|
+
*
|
|
87
|
+
* Usage:
|
|
88
|
+
* import { logger } from "./utils/logger";
|
|
89
|
+
* logger.info("Server started", { port: 3001 });
|
|
90
|
+
* logger.error("Request failed", { path: "/api/test", error: err });
|
|
91
|
+
*
|
|
92
|
+
* Every host global goes through `./host`, and that is load-bearing rather than
|
|
93
|
+
* tidy: this module is reachable from `@rebasepro/server/functions`, the
|
|
94
|
+
* authoring surface that has to import cleanly on a runtime with no `process`.
|
|
95
|
+
* A bare `process.env.NODE_ENV` here would make the first log line of the first
|
|
96
|
+
* request on workerd a `ReferenceError`.
|
|
97
|
+
*/
|
|
5
98
|
/** Google Cloud Logging severity strings. */
|
|
6
99
|
var GCP_SEVERITY = {
|
|
7
100
|
debug: "DEBUG",
|
|
@@ -16,10 +109,10 @@ var LOG_PRIORITY = {
|
|
|
16
109
|
error: 3
|
|
17
110
|
};
|
|
18
111
|
function isProduction() {
|
|
19
|
-
return
|
|
112
|
+
return hostEnv().NODE_ENV === "production";
|
|
20
113
|
}
|
|
21
114
|
function getMinLevel() {
|
|
22
|
-
const env = (
|
|
115
|
+
const env = (hostEnv().LOG_LEVEL || "info").toLowerCase();
|
|
23
116
|
if (env in LOG_PRIORITY) return env;
|
|
24
117
|
return "info";
|
|
25
118
|
}
|
|
@@ -57,7 +150,7 @@ function isSensitiveKey(key) {
|
|
|
57
150
|
* it, and it never re-enables the key deny-list.
|
|
58
151
|
*/
|
|
59
152
|
function rawQueriesAllowed() {
|
|
60
|
-
return
|
|
153
|
+
return hostEnv().NODE_ENV !== "production" && hostEnv().REBASE_LOG_RAW_QUERIES === "true";
|
|
61
154
|
}
|
|
62
155
|
/**
|
|
63
156
|
* Strip every `Failed query: … / params: …` span out of a message or stack,
|
|
@@ -154,8 +247,8 @@ function createLogger(rawDefaultFields = {}) {
|
|
|
154
247
|
...merged
|
|
155
248
|
};
|
|
156
249
|
const line = JSON.stringify(entry);
|
|
157
|
-
if (level === "error")
|
|
158
|
-
else
|
|
250
|
+
if (level === "error") writeLine("err", line);
|
|
251
|
+
else writeLine("out", line);
|
|
159
252
|
} else {
|
|
160
253
|
const prefix = level === "error" ? "❌" : level === "warn" ? "⚠️" : level === "info" ? "ℹ️" : "🐛";
|
|
161
254
|
const extra = Object.keys(merged).length > 0 ? ` ${JSON.stringify(merged)}` : "";
|
|
@@ -185,6 +278,6 @@ function createLogger(rawDefaultFields = {}) {
|
|
|
185
278
|
*/
|
|
186
279
|
var logger = createLogger();
|
|
187
280
|
//#endregion
|
|
188
|
-
export { redactSensitiveText as n, logger as t };
|
|
281
|
+
export { redactSensitiveText as n, hostEnv as r, logger as t };
|
|
189
282
|
|
|
190
|
-
//# sourceMappingURL=logger-
|
|
283
|
+
//# sourceMappingURL=logger-TdvXIGqR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger-TdvXIGqR.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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"}
|
package/dist/metrics/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
import "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
|
-
import {
|
|
4
|
+
import { n as __exportAll } from "./rolldown-runtime-DSJWtz9O.js";
|
|
5
|
+
import { t as logger } from "./logger-TdvXIGqR.js";
|
|
5
6
|
import { Hono } from "hono";
|
|
6
7
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
7
8
|
//#region src/functions/proxy.ts
|
|
@@ -19,6 +20,7 @@ import { getConnInfo } from "@hono/node-server/conninfo";
|
|
|
19
20
|
* `JWT_SECRET` and must see the original `Authorization` header, so re-deciding
|
|
20
21
|
* anything on the way through would put two answers where the system has one.
|
|
21
22
|
*/
|
|
23
|
+
var proxy_exports = /* @__PURE__ */ __exportAll({ createFunctionsProxy: () => createFunctionsProxy });
|
|
22
24
|
/**
|
|
23
25
|
* Headers that describe *this* connection rather than the request, and so must
|
|
24
26
|
* not be copied onto a new one.
|
|
@@ -134,6 +136,6 @@ function socketAddress(c) {
|
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
138
|
//#endregion
|
|
137
|
-
export {
|
|
139
|
+
export { proxy_exports as t };
|
|
138
140
|
|
|
139
|
-
//# sourceMappingURL=proxy-
|
|
141
|
+
//# sourceMappingURL=proxy-QJKSS-CV.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy-
|
|
1
|
+
{"version":3,"file":"proxy-QJKSS-CV.js","names":[],"sources":["../src/functions/proxy.ts"],"sourcesContent":["/**\n * Forwarding `/api/functions/*` to the process that serves it.\n *\n * A split deployment normally puts a reverse proxy in front and routes the path\n * there. This exists so it does not have to: set `REBASE_FUNCTIONS_UPSTREAM` on\n * the API process and the two-container topology presents the identical URL\n * surface as the one-container one, with nothing new to install and nothing for\n * a client or an SDK to know about.\n *\n * It is a transport hop, not a trust boundary. Nothing is authenticated or\n * authorised here: the upstream runs the same auth middleware against the same\n * `JWT_SECRET` and must see the original `Authorization` header, so re-deciding\n * anything on the way through would put two answers where the system has one.\n */\nimport { Hono, type Context } from \"hono\";\nimport { getConnInfo } from \"@hono/node-server/conninfo\";\nimport type { HonoEnv } from \"../api/types\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * Headers that describe *this* connection rather than the request, and so must\n * not be copied onto a new one.\n *\n * `host` is the important one: forwarded unchanged it names the API process, and\n * an upstream doing any virtual-host routing would answer for the wrong service.\n * `content-length` and `transfer-encoding` are recomputed by `fetch` from the\n * body it is actually given, and a stale value is a hang or a truncation.\n */\nconst HOP_BY_HOP = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n \"host\",\n \"content-length\"\n]);\n\n/**\n * Additionally dropped from the *response*, because `fetch` has already acted on\n * them by the time we see the body.\n *\n * `Content-Encoding` is the one that matters and the one that is easy to miss:\n * undici decompresses a `gzip` response transparently, but leaves the header on\n * the `Response` object. Copy it onto the body we hand back and the client is\n * told to gunzip bytes that are already plain — which does not fail cleanly. It\n * hangs, or it surfaces as a decode error several layers away from the proxy\n * that caused it. (This is not hypothetical: the runtime compresses its own\n * responses, so *every* forwarded response carries the header.)\n */\nconst RESPONSE_ONLY_DROP = new Set([\"content-encoding\", \"content-length\"]);\n\nexport interface FunctionsProxyOptions {\n /** Base URL of the process serving the functions, e.g. `http://functions:8080`. */\n upstream: string;\n /** The mount path being forwarded, e.g. `/api/functions`. */\n basePath: string;\n}\n\n/**\n * A router that forwards everything under its mount point to `upstream`.\n *\n * Method, path, query string, body and headers travel verbatim, with two\n * deliberate exceptions:\n *\n * 1. Hop-by-hop headers are dropped ({@link HOP_BY_HOP}).\n * 2. `X-Forwarded-For` gains the address this process saw the request come from.\n * This is not decoration — the upstream's rate limiters and the auth routes'\n * IP logging read that chain, and without the append every forwarded request\n * arrives from one address: the API pod's. Every caller would share a single\n * rate-limit bucket and every login would be logged from the same IP.\n *\n * **The upstream must therefore count one more proxy hop than the API process\n * does.** `TRUSTED_PROXY_HOPS` on the functions process is the API's value\n * plus one. Left unset it defaults to 0, and the upstream falls back to the\n * socket address — again, the API pod's — which is safe but useless.\n */\nexport function createFunctionsProxy(options: FunctionsProxyOptions): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n // Trailing slash trimmed once, here: joining it per request is how a proxy\n // ends up asking for `//functions/x`, which some servers route and some 404.\n const upstream = options.upstream.replace(/\\/+$/, \"\");\n\n router.all(\"/*\", async (c) => {\n const incoming = new URL(c.req.url);\n // The path *below* the mount point. Hono gives the full request path, so\n // taking the suffix is what keeps the upstream's own mount identical:\n // `/api/functions/send-invoice` here becomes `/api/functions/send-invoice`\n // there, not `/api/functions/api/functions/send-invoice`.\n const suffix = incoming.pathname.startsWith(options.basePath)\n ? incoming.pathname.slice(options.basePath.length)\n : incoming.pathname;\n const target = `${upstream}${options.basePath}${suffix}${incoming.search}`;\n\n const headers = new Headers();\n for (const [name, value] of c.req.raw.headers) {\n if (!HOP_BY_HOP.has(name.toLowerCase())) headers.set(name, value);\n }\n\n const seenFrom = socketAddress(c);\n if (seenFrom) {\n const chain = c.req.header(\"x-forwarded-for\");\n headers.set(\"x-forwarded-for\", chain ? `${chain}, ${seenFrom}` : seenFrom);\n }\n\n try {\n const response = await fetch(target, {\n method: c.req.method,\n headers,\n // GET and HEAD may not carry one; everything else streams\n // through rather than being buffered, so a large upload does not\n // have to fit in this process's memory on its way past.\n body: c.req.method === \"GET\" || c.req.method === \"HEAD\" ? undefined : c.req.raw.body,\n // Required by undici whenever the body is a stream. Without it\n // the fetch rejects with a message about duplex that says\n // nothing about what to do.\n duplex: \"half\",\n redirect: \"manual\"\n } as RequestInit);\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders(response.headers)\n });\n } catch (err) {\n // The upstream being down is an infrastructure fact, not a bug in\n // the caller's request, and 502 is the one status that says so.\n // Named explicitly in the body because the alternative — a generic\n // 500 from the API process — sends people reading the wrong logs.\n logger.error(\"Functions upstream unreachable\", {\n upstream,\n path: incoming.pathname,\n error: err instanceof Error ? err : new Error(String(err))\n });\n return c.json({\n error: {\n code: \"FUNCTIONS_UPSTREAM_UNREACHABLE\",\n message: \"The process serving custom functions could not be reached from this one.\"\n }\n }, 502);\n }\n });\n\n return router;\n}\n\n/**\n * Response headers, minus the ones that describe the upstream's connection and\n * the ones `fetch` has already consumed on our behalf.\n */\nfunction responseHeaders(headers: Headers): Headers {\n const out = new Headers();\n for (const [name, value] of headers) {\n const lower = name.toLowerCase();\n if (HOP_BY_HOP.has(lower) || RESPONSE_ONLY_DROP.has(lower)) continue;\n out.set(name, value);\n }\n return out;\n}\n\n/** The address this process saw, or undefined on a runtime that cannot say. */\nfunction socketAddress(c: Context<HonoEnv>): string | undefined {\n try {\n return getConnInfo(c).remote.address;\n } catch {\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,IAAM,6BAAa,IAAI,IAAI;CACvB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ,CAAC;;;;;;;;;;;;;AAcD,IAAM,qCAAqB,IAAI,IAAI,CAAC,oBAAoB,gBAAgB,CAAC;;;;;;;;;;;;;;;;;;;AA2BzE,SAAgB,qBAAqB,SAA+C;CAChF,MAAM,SAAS,IAAI,KAAc;CAGjC,MAAM,WAAW,QAAQ,SAAS,QAAQ,QAAQ,EAAE;CAEpD,OAAO,IAAI,MAAM,OAAO,MAAM;EAC1B,MAAM,WAAW,IAAI,IAAI,EAAE,IAAI,GAAG;EAKlC,MAAM,SAAS,SAAS,SAAS,WAAW,QAAQ,QAAQ,IACtD,SAAS,SAAS,MAAM,QAAQ,SAAS,MAAM,IAC/C,SAAS;EACf,MAAM,SAAS,GAAG,WAAW,QAAQ,WAAW,SAAS,SAAS;EAElE,MAAM,UAAU,IAAI,QAAQ;EAC5B,KAAK,MAAM,CAAC,MAAM,UAAU,EAAE,IAAI,IAAI,SAClC,IAAI,CAAC,WAAW,IAAI,KAAK,YAAY,CAAC,GAAG,QAAQ,IAAI,MAAM,KAAK;EAGpE,MAAM,WAAW,cAAc,CAAC;EAChC,IAAI,UAAU;GACV,MAAM,QAAQ,EAAE,IAAI,OAAO,iBAAiB;GAC5C,QAAQ,IAAI,mBAAmB,QAAQ,GAAG,MAAM,IAAI,aAAa,QAAQ;EAC7E;EAEA,IAAI;GACA,MAAM,WAAW,MAAM,MAAM,QAAQ;IACjC,QAAQ,EAAE,IAAI;IACd;IAIA,MAAM,EAAE,IAAI,WAAW,SAAS,EAAE,IAAI,WAAW,SAAS,KAAA,IAAY,EAAE,IAAI,IAAI;IAIhF,QAAQ;IACR,UAAU;GACd,CAAgB;GAEhB,OAAO,IAAI,SAAS,SAAS,MAAM;IAC/B,QAAQ,SAAS;IACjB,YAAY,SAAS;IACrB,SAAS,gBAAgB,SAAS,OAAO;GAC7C,CAAC;EACL,SAAS,KAAK;GAKV,OAAO,MAAM,kCAAkC;IAC3C;IACA,MAAM,SAAS;IACf,OAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;GAC7D,CAAC;GACD,OAAO,EAAE,KAAK,EACV,OAAO;IACH,MAAM;IACN,SAAS;GACb,EACJ,GAAG,GAAG;EACV;CACJ,CAAC;CAED,OAAO;AACX;;;;;AAMA,SAAS,gBAAgB,SAA2B;CAChD,MAAM,MAAM,IAAI,QAAQ;CACxB,KAAK,MAAM,CAAC,MAAM,UAAU,SAAS;EACjC,MAAM,QAAQ,KAAK,YAAY;EAC/B,IAAI,WAAW,IAAI,KAAK,KAAK,mBAAmB,IAAI,KAAK,GAAG;EAC5D,IAAI,IAAI,MAAM,KAAK;CACvB;CACA,OAAO;AACX;;AAGA,SAAS,cAAc,GAAyC;CAC5D,IAAI;EACA,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO;CACjC,QAAQ;EACJ;CACJ;AACJ"}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
|
-
import {
|
|
4
|
+
import { n as __exportAll } from "./rolldown-runtime-DSJWtz9O.js";
|
|
5
|
+
import { t as logger } from "./logger-TdvXIGqR.js";
|
|
5
6
|
//#region src/functions/request-timeout.ts
|
|
7
|
+
var request_timeout_exports = /* @__PURE__ */ __exportAll({
|
|
8
|
+
DEFAULT_FUNCTIONS_TIMEOUT_MS: () => DEFAULT_FUNCTIONS_TIMEOUT_MS,
|
|
9
|
+
createFunctionsRequestTimeout: () => createFunctionsRequestTimeout,
|
|
10
|
+
resolveFunctionsTimeoutMs: () => resolveFunctionsTimeoutMs
|
|
11
|
+
});
|
|
6
12
|
/** Default ceiling for a custom function request, in milliseconds. */
|
|
7
13
|
var DEFAULT_FUNCTIONS_TIMEOUT_MS = 3e4;
|
|
8
14
|
/**
|
|
@@ -39,6 +45,14 @@ function resolveFunctionsTimeoutMs(configured) {
|
|
|
39
45
|
* eventual result is dropped. It is a ceiling, not a kill switch, and the exact
|
|
40
46
|
* number matters far less than its existence.
|
|
41
47
|
*
|
|
48
|
+
* "The handler keeps running" is a **Node** guarantee, not a property of the
|
|
49
|
+
* contract. It follows from the process outliving the request, which is not
|
|
50
|
+
* true on an isolate-based host: there, work still in flight when the response
|
|
51
|
+
* resolves is terminated rather than orphaned. So a handler must not depend on
|
|
52
|
+
* finishing after its 504 — anything that has to complete belongs in
|
|
53
|
+
* `waitUntil()`, which is the one construct both hosts honour. See
|
|
54
|
+
* `./wait-until.ts`.
|
|
55
|
+
*
|
|
42
56
|
* Mounted in front of the auth middleware rather than behind it, so a wedged
|
|
43
57
|
* driver — the failure that also hangs `withAuth()` — is covered too.
|
|
44
58
|
*/
|
|
@@ -63,6 +77,6 @@ function createFunctionsRequestTimeout(ms) {
|
|
|
63
77
|
};
|
|
64
78
|
}
|
|
65
79
|
//#endregion
|
|
66
|
-
export {
|
|
80
|
+
export { request_timeout_exports as t };
|
|
67
81
|
|
|
68
|
-
//# sourceMappingURL=request-timeout-
|
|
82
|
+
//# sourceMappingURL=request-timeout-OofPCHQT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-timeout-OofPCHQT.js","names":[],"sources":["../src/functions/request-timeout.ts"],"sourcesContent":["import type { MiddlewareHandler } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { logger } from \"../utils/logger\";\n\n/** Default ceiling for a custom function request, in milliseconds. */\nexport const DEFAULT_FUNCTIONS_TIMEOUT_MS = 30_000;\n\n/**\n * Resolve the functions request timeout from config, then env, then the default.\n *\n * `0` (or any non-positive number) disables the ceiling — for a deployment\n * whose proxy already imposes one, or a function that legitimately streams for\n * minutes.\n */\nexport function resolveFunctionsTimeoutMs(configured?: number): number {\n if (typeof configured === \"number\" && Number.isFinite(configured)) {\n return Math.max(0, Math.floor(configured));\n }\n // Blank means unset, not zero. `Number(\"\")` and `Number(\" \")` are both 0,\n // and 0 here means \"no ceiling\" — so a compose file with\n // `REBASE_FUNCTIONS_TIMEOUT_MS=${SOMETHING}` and `SOMETHING` undefined, or\n // a `.env` line with the name and no value, silently switched off the one\n // bound on how long code the framework did not write may hold a socket.\n // Declaring a variable without setting it is the ordinary way to write\n // both of those files, and the failure is invisible: nothing logs, and the\n // deployment behaves exactly as it did before the ceiling existed.\n const raw = process.env.REBASE_FUNCTIONS_TIMEOUT_MS?.trim();\n if (raw) {\n const fromEnv = Number(raw);\n if (Number.isFinite(fromEnv) && fromEnv >= 0) {\n return Math.floor(fromEnv);\n }\n }\n return DEFAULT_FUNCTIONS_TIMEOUT_MS;\n}\n\n/**\n * A per-request ceiling for the custom functions router.\n *\n * Custom functions are the one router that runs code the framework did not\n * write, and nothing else in the stack bounds how long that code takes: the\n * Node server is constructed without `requestTimeout`/`headersTimeout`, so a\n * handler awaiting a promise that never settles — a `fetch` to an unreachable\n * third party with no `AbortSignal`, a query on a wedged connection — holds its\n * socket and its request object until the client gives up. On the managed\n * runtime the process is shared between tenants, so that is not only the slow\n * caller's problem.\n *\n * The handler is **not** cancelled — it cannot be, there is no cancellation\n * token to hand user code. What is bounded is the client-visible request: after\n * `ms` the caller gets a 504 and the socket is released, and the handler's\n * eventual result is dropped. It is a ceiling, not a kill switch, and the exact\n * number matters far less than its existence.\n *\n * \"The handler keeps running\" is a **Node** guarantee, not a property of the\n * contract. It follows from the process outliving the request, which is not\n * true on an isolate-based host: there, work still in flight when the response\n * resolves is terminated rather than orphaned. So a handler must not depend on\n * finishing after its 504 — anything that has to complete belongs in\n * `waitUntil()`, which is the one construct both hosts honour. See\n * `./wait-until.ts`.\n *\n * Mounted in front of the auth middleware rather than behind it, so a wedged\n * driver — the failure that also hangs `withAuth()` — is covered too.\n */\nexport function createFunctionsRequestTimeout(ms: number): MiddlewareHandler<HonoEnv> {\n return async (c, next) => {\n if (ms <= 0) return next();\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), ms);\n });\n\n try {\n const outcome = await Promise.race([next().then(() => \"done\" as const), timedOut]);\n if (outcome === \"timeout\") {\n logger.warn(\n `[functions] ${c.req.method} ${c.req.path} exceeded the ${ms}ms request timeout — ` +\n \"answering 504. The handler is still running; it cannot be cancelled from here. \" +\n \"Give outbound calls an AbortSignal, or raise `functionsTimeoutMs` / REBASE_FUNCTIONS_TIMEOUT_MS.\"\n );\n return c.json({\n error: {\n message: \"Function timed out\",\n code: \"FUNCTION_TIMEOUT\"\n }\n }, 504);\n }\n } finally {\n if (timer) clearTimeout(timer);\n }\n return undefined;\n };\n}\n"],"mappings":";;;;;;;;;;;;AAKA,IAAa,+BAA+B;;;;;;;;AAS5C,SAAgB,0BAA0B,YAA6B;CACnE,IAAI,OAAO,eAAe,YAAY,OAAO,SAAS,UAAU,GAC5D,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,CAAC;CAU7C,MAAM,MAAM,QAAQ,IAAI,6BAA6B,KAAK;CAC1D,IAAI,KAAK;EACL,MAAM,UAAU,OAAO,GAAG;EAC1B,IAAI,OAAO,SAAS,OAAO,KAAK,WAAW,GACvC,OAAO,KAAK,MAAM,OAAO;CAEjC;CACA,OAAO;AACX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,8BAA8B,IAAwC;CAClF,OAAO,OAAO,GAAG,SAAS;EACtB,IAAI,MAAM,GAAG,OAAO,KAAK;EAEzB,IAAI;EACJ,MAAM,WAAW,IAAI,SAAoB,YAAY;GACjD,QAAQ,iBAAiB,QAAQ,SAAS,GAAG,EAAE;EACnD,CAAC;EAED,IAAI;GAEA,IAAI,MADkB,QAAQ,KAAK,CAAC,KAAK,CAAC,CAAC,WAAW,MAAe,GAAG,QAAQ,CAAC,MACjE,WAAW;IACvB,OAAO,KACH,eAAe,EAAE,IAAI,OAAO,GAAG,EAAE,IAAI,KAAK,gBAAgB,GAAG,uMAGjE;IACA,OAAO,EAAE,KAAK,EACV,OAAO;KACH,SAAS;KACT,MAAM;IACV,EACJ,GAAG,GAAG;GACV;EACJ,UAAU;GACN,IAAI,OAAO,aAAa,KAAK;EACjC;CAEJ;AACJ"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape `@rebasepro/rls-check` returns. Declared structurally rather than
|
|
3
|
+
* imported as a type, so this module carries no dependency on it at all — see
|
|
4
|
+
* the module comment.
|
|
5
|
+
*/
|
|
6
|
+
export interface RlsScanFinding {
|
|
7
|
+
id: string;
|
|
8
|
+
severity: "info" | "low" | "medium" | "high" | "critical";
|
|
9
|
+
title: string;
|
|
10
|
+
target: {
|
|
11
|
+
schema: string;
|
|
12
|
+
table?: string;
|
|
13
|
+
policy?: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface RlsScanResult {
|
|
17
|
+
scannedAt: string;
|
|
18
|
+
/** Host and database name only — the checker never returns credentials. */
|
|
19
|
+
database: {
|
|
20
|
+
host: string;
|
|
21
|
+
name: string;
|
|
22
|
+
};
|
|
23
|
+
stats: {
|
|
24
|
+
schemas: number;
|
|
25
|
+
tables: number;
|
|
26
|
+
policies: number;
|
|
27
|
+
tablesWithoutRls: number;
|
|
28
|
+
checksRun: number;
|
|
29
|
+
};
|
|
30
|
+
findings: RlsScanFinding[];
|
|
31
|
+
}
|
|
32
|
+
declare const SEVERITY_ORDER: readonly ["info", "low", "medium", "high", "critical"];
|
|
33
|
+
export type RlsSeverity = (typeof SEVERITY_ORDER)[number];
|
|
34
|
+
export interface RlsAuditConfig {
|
|
35
|
+
/** Off unless set. See the module comment for why. */
|
|
36
|
+
enabled?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* How often to run, in milliseconds. Default 24 hours.
|
|
39
|
+
*
|
|
40
|
+
* The thing being watched is a schema, which changes on deploys, not on
|
|
41
|
+
* traffic — so this is a daily safety net, not a monitor.
|
|
42
|
+
*/
|
|
43
|
+
intervalMs?: number;
|
|
44
|
+
/** Run once at startup as well as on the interval. Default true. */
|
|
45
|
+
runOnBoot?: boolean;
|
|
46
|
+
/** Restrict the scan to these schemas. Default: every user schema. */
|
|
47
|
+
schemas?: string[];
|
|
48
|
+
/**
|
|
49
|
+
* Where to connect. Defaults to `DATABASE_URL`.
|
|
50
|
+
*
|
|
51
|
+
* Taken as a string rather than borrowed from the driver because the checker
|
|
52
|
+
* connects on its own terms — its own session, its own statement timeout —
|
|
53
|
+
* and threading it through a driver would make the audit's behaviour depend
|
|
54
|
+
* on the pool it borrowed from.
|
|
55
|
+
*/
|
|
56
|
+
connectionString?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Severity at or above which a completed run logs a warning rather than an
|
|
59
|
+
* info line. Default `high`.
|
|
60
|
+
*/
|
|
61
|
+
warnAtSeverity?: RlsSeverity;
|
|
62
|
+
/** Statement timeout for the scan's own queries. Default 30s. */
|
|
63
|
+
statementTimeoutMs?: number;
|
|
64
|
+
/**
|
|
65
|
+
* The scanner. Pass `scan` from `@rebasepro/rls-check`:
|
|
66
|
+
*
|
|
67
|
+
* ```ts
|
|
68
|
+
* import { scan } from "@rebasepro/rls-check";
|
|
69
|
+
* rlsAudit: { enabled: true, scan }
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* Required when `enabled`. See the module comment for why it is not
|
|
73
|
+
* imported here.
|
|
74
|
+
*/
|
|
75
|
+
scan?: RlsScanner;
|
|
76
|
+
}
|
|
77
|
+
/** The one function this module needs from `@rebasepro/rls-check`. */
|
|
78
|
+
export type RlsScanner = (options: {
|
|
79
|
+
connectionString: string;
|
|
80
|
+
schemas?: string[];
|
|
81
|
+
statementTimeoutMs?: number;
|
|
82
|
+
}) => Promise<RlsScanResult>;
|
|
83
|
+
export interface RlsAuditStatus {
|
|
84
|
+
enabled: boolean;
|
|
85
|
+
/** Why it is not running, when it is not. */
|
|
86
|
+
reason?: string;
|
|
87
|
+
lastRunAt?: string;
|
|
88
|
+
lastError?: string;
|
|
89
|
+
result?: RlsScanResult;
|
|
90
|
+
}
|
|
91
|
+
export interface RlsAudit {
|
|
92
|
+
start(): void;
|
|
93
|
+
stop(): void;
|
|
94
|
+
/** Run now, outside the schedule. Resolves when the run finishes. */
|
|
95
|
+
runNow(): Promise<void>;
|
|
96
|
+
status(): RlsAuditStatus;
|
|
97
|
+
}
|
|
98
|
+
/** One line naming what the run found, at the right level to be noticed. */
|
|
99
|
+
export declare function summarize(result: RlsScanResult, warnAt: RlsSeverity): {
|
|
100
|
+
level: "warn" | "info";
|
|
101
|
+
message: string;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Create the audit. Nothing runs until {@link RlsAudit.start}.
|
|
105
|
+
*
|
|
106
|
+
* Because the scanner is supplied by the caller, the schedule, the summary and
|
|
107
|
+
* the failure handling are all testable without a database and without
|
|
108
|
+
* `@rebasepro/rls-check` present.
|
|
109
|
+
*/
|
|
110
|
+
export declare function createRlsAudit(config: RlsAuditConfig): RlsAudit;
|
|
111
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit the schema change, then apply it.
|
|
3
|
+
*
|
|
4
|
+
* The order is the load-bearing part of this module and the reason it exists as
|
|
5
|
+
* a unit rather than as two calls a caller makes in whichever order.
|
|
6
|
+
*
|
|
7
|
+
* The two failure directions are not symmetric:
|
|
8
|
+
*
|
|
9
|
+
* - **Apply first, commit fails.** The database has a column the repository does
|
|
10
|
+
* not describe. The ensure path is strictly additive — it never drops
|
|
11
|
+
* anything — so the next deploy neither removes the column nor mentions it. It
|
|
12
|
+
* sits there, absent from the collections and invisible to the API, until
|
|
13
|
+
* somebody goes looking. Nothing in the system detects this state.
|
|
14
|
+
* - **Commit first, apply fails.** The repository describes a column the
|
|
15
|
+
* database does not have. That is the ordinary state of every project between
|
|
16
|
+
* an edit and a deploy, and boot's ensure reconciles it on the next start.
|
|
17
|
+
*
|
|
18
|
+
* So: commit, then apply. The bad half of the dual write lands in the direction
|
|
19
|
+
* the system already handles, and a failed apply is reported as a *state* rather
|
|
20
|
+
* than thrown as an error — because "committed, will apply on next boot" is not
|
|
21
|
+
* a failure, it is a slower success.
|
|
22
|
+
*
|
|
23
|
+
* ## Both dependencies are injected
|
|
24
|
+
*
|
|
25
|
+
* `git` and `apply` are interfaces, not imports. That keeps this testable
|
|
26
|
+
* without a repository or a database — which matters, because the thing worth
|
|
27
|
+
* testing here is the ordering and what happens when half of it fails, and both
|
|
28
|
+
* are impossible to exercise against real infrastructure on demand.
|
|
29
|
+
*/
|
|
30
|
+
import type { SchemaChangeFile, SchemaChangePlan, ClassifiedSchemaChanges } from "@rebasepro/types";
|
|
31
|
+
/** The working tree the commit lands in. */
|
|
32
|
+
export interface SchemaEditRepository {
|
|
33
|
+
/** Absolute path, for reporting. */
|
|
34
|
+
root: string;
|
|
35
|
+
/** The branch the commit will land on. */
|
|
36
|
+
currentBranch(): Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Paths that are already modified and not ours.
|
|
39
|
+
*
|
|
40
|
+
* A commit that sweeps up somebody's half-finished work is worse than a
|
|
41
|
+
* refusal, and this is the one thing a schema editor cannot see coming.
|
|
42
|
+
*/
|
|
43
|
+
dirtyPaths(): Promise<string[]>;
|
|
44
|
+
/** Write every file, creating directories as needed. */
|
|
45
|
+
writeFiles(files: SchemaChangeFile[]): Promise<void>;
|
|
46
|
+
/** Stage exactly these paths and commit them. Returns the new sha. */
|
|
47
|
+
commit(paths: string[], message: string): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* The current contents of one file, or `undefined` when it does not exist.
|
|
50
|
+
*
|
|
51
|
+
* Needed by a deployment whose source is not on the machine. The AST editor
|
|
52
|
+
* rewrites a collection *file*, so a bundle — which ships compiled output —
|
|
53
|
+
* has nothing for it to open; the file has to come from the repository
|
|
54
|
+
* first. A missing file is not an error: a new collection has no source yet,
|
|
55
|
+
* and the editor creates one.
|
|
56
|
+
*
|
|
57
|
+
* Optional. A local working tree could implement it and does not need to:
|
|
58
|
+
* the file is already on the disk the editor reads.
|
|
59
|
+
*/
|
|
60
|
+
readFile?(path: string): Promise<string | undefined>;
|
|
61
|
+
}
|
|
62
|
+
/** Runs the DDL. Separate from the repository so neither knows about the other. */
|
|
63
|
+
export type SchemaEditApply = (statements: string[]) => Promise<void>;
|
|
64
|
+
export interface SchemaEditInput {
|
|
65
|
+
/**
|
|
66
|
+
* What to write and run, from `admin.planSchemaChange`.
|
|
67
|
+
*
|
|
68
|
+
* Taken as a plan rather than as collections because planning is
|
|
69
|
+
* engine-specific and this module is not: it commits files and runs
|
|
70
|
+
* statements, and does not care which database rendered them.
|
|
71
|
+
*/
|
|
72
|
+
plan: SchemaChangePlan;
|
|
73
|
+
repository: SchemaEditRepository;
|
|
74
|
+
apply: SchemaEditApply;
|
|
75
|
+
/**
|
|
76
|
+
* Rewrite the collection source, returning the files it touched.
|
|
77
|
+
*
|
|
78
|
+
* Called from **inside** this module, after the dirty-tree check and before
|
|
79
|
+
* the commit. It has to be: the AST editor writes through the filesystem
|
|
80
|
+
* rather than through {@link SchemaEditRepository}, so a caller that wrote
|
|
81
|
+
* first and handed the files over would already have made the tree dirty,
|
|
82
|
+
* and the check below would refuse the change on the evidence of its own
|
|
83
|
+
* edit.
|
|
84
|
+
*
|
|
85
|
+
* That is not hypothetical — it is what `/apply` did. Every change was
|
|
86
|
+
* refused with a dirty tree, because `git status --porcelain` reports
|
|
87
|
+
* untracked files too and a new collection's source file is always
|
|
88
|
+
* untracked. The rewritten file was left behind either way, so the retry
|
|
89
|
+
* found a dirty tree as well and the surface could never succeed.
|
|
90
|
+
*
|
|
91
|
+
* Optional: a caller holding the contents already, with no disk to write
|
|
92
|
+
* them through, puts them on `plan.files` and omits this.
|
|
93
|
+
*/
|
|
94
|
+
writeSource?: () => Promise<SchemaChangeFile[]>;
|
|
95
|
+
/**
|
|
96
|
+
* The paths {@link writeSource} is going to touch.
|
|
97
|
+
*
|
|
98
|
+
* Needed *before* it runs so they can join the dirty check — the point of
|
|
99
|
+
* which is to read the tree before this change has touched it. The caller
|
|
100
|
+
* can derive them (`<collectionsDir>/<id>.ts`) without writing anything.
|
|
101
|
+
*/
|
|
102
|
+
sourcePaths?: string[];
|
|
103
|
+
}
|
|
104
|
+
export interface SchemaEditResult {
|
|
105
|
+
committed: {
|
|
106
|
+
sha: string;
|
|
107
|
+
branch: string;
|
|
108
|
+
files: string[];
|
|
109
|
+
};
|
|
110
|
+
/** True when the DDL ran. False means committed and pending a boot. */
|
|
111
|
+
applied: boolean;
|
|
112
|
+
/** Why the apply did not run, when it did not. Never a reason to fail. */
|
|
113
|
+
applyError?: string;
|
|
114
|
+
statements: string[];
|
|
115
|
+
classified: ClassifiedSchemaChanges;
|
|
116
|
+
/** What to tell the person who pressed the button. */
|
|
117
|
+
summary: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The plan says the change is not applicable.
|
|
121
|
+
*
|
|
122
|
+
* Re-checked here rather than trusted from the planner: this module is the one
|
|
123
|
+
* that writes and runs things, so the guarantee belongs where the consequence
|
|
124
|
+
* is. A caller that built a plan by hand cannot route around it.
|
|
125
|
+
*/
|
|
126
|
+
export declare class UnapplicableChangeError extends Error {
|
|
127
|
+
readonly classified: ClassifiedSchemaChanges;
|
|
128
|
+
constructor(message: string, classified: ClassifiedSchemaChanges);
|
|
129
|
+
}
|
|
130
|
+
export declare class DirtyWorkingTreeError extends Error {
|
|
131
|
+
readonly paths: string[];
|
|
132
|
+
constructor(paths: string[]);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Generate the commit, land it, then run the DDL.
|
|
136
|
+
*
|
|
137
|
+
* Throws only for things that mean *nothing happened*: a change the ensure path
|
|
138
|
+
* cannot express, a dirty tree, or a failed commit. Once the commit lands,
|
|
139
|
+
* every outcome is a result.
|
|
140
|
+
*/
|
|
141
|
+
export declare function applySchemaChange(input: SchemaEditInput): Promise<SchemaEditResult>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { SchemaEditRepository } from "./apply-schema-change.js";
|
|
2
|
+
/**
|
|
3
|
+
* How to authenticate to GitHub.
|
|
4
|
+
*
|
|
5
|
+
* Two ways, because there are two deployments. A Cloud tenant is one of many
|
|
6
|
+
* behind a single app, so it authenticates as an *installation* of that app and
|
|
7
|
+
* holds no credential of its own. A self-hoster running a bundle has the same
|
|
8
|
+
* problem — no source on the machine — and no app: standing one up so that a
|
|
9
|
+
* server can commit to a repository they already own would be a great deal of
|
|
10
|
+
* ceremony around a one-line credential.
|
|
11
|
+
*
|
|
12
|
+
* Both end in the same place. Everything past the bearer token is the plain Git
|
|
13
|
+
* Data API and is identical for either, which is the point: the cloud is a
|
|
14
|
+
* better implementation of this interface, never the only one.
|
|
15
|
+
*/
|
|
16
|
+
export type GitHubAuth = {
|
|
17
|
+
kind: "app";
|
|
18
|
+
/** Numeric app id, or the client id — GitHub accepts either as `iss`. */
|
|
19
|
+
appId: string;
|
|
20
|
+
/**
|
|
21
|
+
* The app's private key, PEM.
|
|
22
|
+
*
|
|
23
|
+
* Accepts the literal PEM, a PEM with escaped newlines, or base64 —
|
|
24
|
+
* pass it through `normalizePemFromEnv` first, which is what the boot
|
|
25
|
+
* path does.
|
|
26
|
+
*/
|
|
27
|
+
privateKey: string;
|
|
28
|
+
/** Which installation to act as. Produced when the app is installed. */
|
|
29
|
+
installationId: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: "token";
|
|
32
|
+
/**
|
|
33
|
+
* A personal access token, or a fine-grained token with write access to
|
|
34
|
+
* the repository's contents. Used as the bearer directly — there is
|
|
35
|
+
* nothing to exchange and nothing to cache.
|
|
36
|
+
*/
|
|
37
|
+
token: string;
|
|
38
|
+
};
|
|
39
|
+
export interface GitHubRepositoryOptions {
|
|
40
|
+
auth: GitHubAuth;
|
|
41
|
+
owner: string;
|
|
42
|
+
repo: string;
|
|
43
|
+
/** Branch to commit onto. */
|
|
44
|
+
branch: string;
|
|
45
|
+
/** Attribution for the commit. */
|
|
46
|
+
author?: {
|
|
47
|
+
name: string;
|
|
48
|
+
email: string;
|
|
49
|
+
};
|
|
50
|
+
/** Injected for tests. Defaults to the global. */
|
|
51
|
+
fetchImpl?: typeof fetch;
|
|
52
|
+
/** Injected so token expiry is testable without waiting an hour. */
|
|
53
|
+
now?: () => number;
|
|
54
|
+
}
|
|
55
|
+
export declare class GitHubApiError extends Error {
|
|
56
|
+
readonly status: number;
|
|
57
|
+
readonly path: string;
|
|
58
|
+
constructor(status: number, path: string, detail: string);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A JWT authenticating the app itself.
|
|
62
|
+
*
|
|
63
|
+
* `iat` is backdated by a minute: GitHub rejects a token whose `iat` is in its
|
|
64
|
+
* future, and a server clock a few seconds fast is enough to trigger it.
|
|
65
|
+
*/
|
|
66
|
+
export declare function createAppJwt(appId: string, privateKey: string, nowMs: number): string;
|
|
67
|
+
export declare function createGitHubRepository(options: GitHubRepositoryOptions): SchemaEditRepository;
|