@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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"logger-DfvF_8r-.js","names":[],"sources":["../src/utils/logger.ts"],"sourcesContent":["/**\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\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 process.env.NODE_ENV === \"production\";\n}\n\nfunction getMinLevel(): LogLevel {\n const env = (process.env.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 process.env.NODE_ENV !== \"production\"\n && process.env.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 process.stderr.write(line + \"\\n\");\n } else {\n process.stdout.write(line + \"\\n\");\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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"logs-routes-CWBLQj2l.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\";\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 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\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: query.limit ? parseInt(query.limit) : undefined,\n offset: query.offset ? parseInt(query.offset) : undefined,\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 = parseInt(c.req.query(\"count\") || \"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 = query.limit ? parseInt(query.limit) : 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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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"request-timeout-BuFoEKwT.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 * 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;;;;;;;;;;;;;;;;;;;;;;AAuBA,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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"schema-editor-routes-CV9k0w3G.js","names":[],"sources":["../src/api/ast-schema-editor.ts","../src/api/schema-editor-routes.ts"],"sourcesContent":["import { Project, SyntaxKind, Node, ObjectLiteralExpression, ObjectLiteralElementLike, PropertyAssignment, SourceFile, IndentationText } from \"ts-morph\";\nimport { nestAdminCollectionKeys, nestAdminPropertyKeys } from \"@rebasepro/types\";\nimport * as path from \"path\";\nimport * as fs from \"fs\";\n\n/**\n * The helpers a collection file may be wrapped in.\n *\n * `rebase init` scaffolds every collection as\n * `const postsCollection = defineCollection({ … })` — a call expression, not the\n * bare object literal `rebase introspect` emits. An editor that only understood\n * the bare form found nothing to patch in any stock project, and then rewrote the\n * file from the panel's JSON: no wrapper, no imports, no relation thunks.\n */\nconst COLLECTION_FACTORIES = new Set([\"defineCollection\"]);\n\n/**\n * A value that must be emitted as source code rather than as JSON.\n *\n * Everything reaching the writer has been through `JSON.stringify` on the wire,\n * so a function-valued key arrives either missing or as a string. A relation's\n * `target` is a thunk in the file and a slug in the payload; this is how the\n * thunk gets written back.\n */\nclass RawExpression {\n constructor(public readonly text: string) {\n }\n}\n\n/**\n * Move presentation keys into the `admin` block.\n *\n * The rule itself lives in `@rebasepro/types`, next to `ADMIN_COLLECTION_KEYS`,\n * because `@rebasepro/admin-types` has to apply the identical one on the panel's\n * side and this package cannot import that one. Two copies used to exist and\n * they disagreed about precedence, which decided whether a presentation edit was\n * saved or silently reverted to the value the user had just changed away from.\n */\nexport function nestAdminKeys(collectionData: Record<string, unknown>): Record<string, unknown> {\n return nestAdminCollectionKeys(collectionData);\n}\n\nexport class AstSchemaEditor {\n private project: Project;\n private collectionsDir: string;\n\n constructor(collectionsDir: string) {\n this.project = new Project({\n manipulationSettings: {\n indentationText: IndentationText.FourSpaces\n }\n });\n if (fs.existsSync(collectionsDir)) {\n this.project.addSourceFilesAtPaths(`${collectionsDir}/**/*.ts`);\n }\n this.collectionsDir = path.resolve(collectionsDir);\n }\n\n /**\n * Sanitize collectionId to prevent path traversal attacks.\n * Only allows alphanumeric characters, underscores, and hyphens.\n */\n private sanitizeCollectionId(collectionId: string): string {\n const sanitized = collectionId.replace(/[^a-zA-Z0-9_-]/g, \"\");\n if (!sanitized || sanitized !== collectionId) {\n throw new Error(`Invalid collection ID: \"${collectionId}\". Only alphanumeric characters, underscores, and hyphens are allowed.`);\n }\n return sanitized;\n }\n\n /**\n * Resolve a file path and ensure it falls within the collectionsDir.\n */\n private safePath(filename: string): string {\n const resolved = path.resolve(this.collectionsDir, filename);\n if (!resolved.startsWith(this.collectionsDir + path.sep) && resolved !== this.collectionsDir) {\n throw new Error(\"Path traversal detected: resolved path is outside the collections directory.\");\n }\n return resolved;\n }\n\n private getCollectionFile(collectionId: string) {\n const safeId = this.sanitizeCollectionId(collectionId);\n const filePath = this.safePath(`${safeId}.ts`);\n let file = this.project.getSourceFile(filePath);\n if (!file && fs.existsSync(filePath)) {\n this.project.addSourceFilesAtPaths(`${this.collectionsDir}/**/*.ts`);\n file = this.project.getSourceFile(filePath);\n }\n return file;\n }\n\n /**\n * Find the object literal a collection is declared with, through whatever\n * wraps it.\n *\n * `defineCollection({ … })` is the shape `rebase init` writes and the one the\n * docs recommend; `satisfies` / `as` / parentheses are the other ways an\n * author can dress the same literal. Returning `null` for any of them meant\n * the three callers below each failed differently, and the worst of them\n * overwrote the file.\n */\n private unwrapCollectionObject(node: Node | undefined): ObjectLiteralExpression | null {\n if (!node) return null;\n if (Node.isObjectLiteralExpression(node)) return node;\n if (Node.isParenthesizedExpression(node) ||\n Node.isAsExpression(node) ||\n Node.isSatisfiesExpression(node) ||\n Node.isTypeAssertion(node) ||\n Node.isNonNullExpression(node)) {\n return this.unwrapCollectionObject(node.getExpression());\n }\n if (Node.isCallExpression(node)) {\n // `defineCollection`, `admin.defineCollection`, `defineCollection<Post>`\n const callee = node.getExpression().getText().split(\".\").pop();\n if (callee && COLLECTION_FACTORIES.has(callee)) {\n return this.unwrapCollectionObject(node.getArguments()[0]);\n }\n }\n return null;\n }\n\n private getCollectionObject(collectionId: string): ObjectLiteralExpression | null {\n const file = this.getCollectionFile(collectionId);\n if (!file) return null;\n\n const defaultExport = file.getDefaultExportSymbol();\n if (defaultExport) {\n const declaration = defaultExport.getDeclarations()[0];\n if (declaration && declaration.getKind() === SyntaxKind.ExportAssignment) {\n const expr = declaration.asKind(SyntaxKind.ExportAssignment)?.getExpression();\n if (expr && expr.getKind() === SyntaxKind.Identifier) {\n const varName = expr.getText();\n const varDecl = file.getVariableDeclaration(varName);\n const unwrapped = this.unwrapCollectionObject(varDecl?.getInitializer());\n if (unwrapped) return unwrapped;\n } else {\n // `export default defineCollection({ … })`\n const unwrapped = this.unwrapCollectionObject(expr);\n if (unwrapped) return unwrapped;\n }\n }\n }\n // Fallback: the first VariableDeclaration that holds a collection literal\n for (const varDecl of file.getVariableDeclarations()) {\n const init = this.unwrapCollectionObject(varDecl.getInitializer());\n if (init) return init;\n }\n return null;\n }\n\n /**\n * The collection's object literal, or a refusal that says what to do.\n *\n * Every caller needs this to be all-or-nothing: a missing object literal used\n * to mean \"throw\", \"report success and do nothing\" and \"recreate the file\n * from scratch\" depending on which method you called.\n */\n private requireCollectionObject(collectionId: string): ObjectLiteralExpression {\n const file = this.getCollectionFile(collectionId);\n if (!file) {\n throw new Error(`Collection \"${collectionId}\" has no file at ${path.join(this.collectionsDir, `${collectionId}.ts`)}.`);\n }\n const collectionObj = this.getCollectionObject(collectionId);\n if (!collectionObj) {\n throw new Error(this.unreadableFileMessage(collectionId, file));\n }\n return collectionObj;\n }\n\n private unreadableFileMessage(collectionId: string, file: SourceFile): string {\n return `Could not find the collection object in ${file.getFilePath()}. ` +\n \"The schema editor can only edit a collection declared as `const x = defineCollection({ … })` \" +\n \"or `const x: CollectionConfig = { … }` and exported as the file's default. \" +\n `Edit \"${collectionId}\" in code instead.`;\n }\n\n /** Look a key up on an object literal, quoted or not. */\n private findProperty(obj: ObjectLiteralExpression, name: string): ObjectLiteralElementLike | undefined {\n return obj.getProperty((p: ObjectLiteralElementLike) =>\n \"getName\" in p &&\n typeof (p as PropertyAssignment).getName === \"function\" &&\n ((p as PropertyAssignment).getName() === name ||\n (p as PropertyAssignment).getName() === `\"${name}\"` ||\n (p as PropertyAssignment).getName() === `'${name}'`));\n }\n\n private static quoteKey(key: string): string {\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);\n }\n\n private static isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value) && !(value instanceof RawExpression);\n }\n\n private convertJsonToAstString(obj: unknown, indentLevel = 0, oldAstNode?: ObjectLiteralExpression): string {\n // Base TS-morph parses arrays as 2 levels deep from the property key:\n // PropertiesObject = level 1, PropertyConfig = level 2.\n // We calibrate the spacing multiples to keep the items flush with standard TS format.\n const indentStr = \" \";\n const indent = indentStr.repeat(indentLevel);\n const innerIndent = indentStr.repeat(indentLevel + 1);\n\n if (obj instanceof RawExpression) {\n return obj.text;\n }\n if (obj === null || obj === undefined) {\n return \"undefined\";\n }\n if (typeof obj === \"string\") {\n return JSON.stringify(obj);\n }\n if (typeof obj === \"number\" || typeof obj === \"boolean\") {\n return String(obj);\n }\n if (Array.isArray(obj)) {\n if (obj.length === 0) return \"[]\";\n const items = obj.map(item => this.convertJsonToAstString(item, indentLevel + 1));\n return `[\\n${innerIndent}${items.join(`,\\n${innerIndent}`)}\\n${indent}]`;\n }\n if (typeof obj === \"object\") {\n const record = obj as Record<string, unknown>;\n const keys = Object.keys(record);\n\n // Collect preserved AST properties\n const preservedProps: string[] = [];\n if (oldAstNode) {\n const oldProps = oldAstNode.getProperties();\n for (const oldProp of oldProps) {\n if (oldProp.isKind(SyntaxKind.PropertyAssignment)) {\n const nameNode = oldProp.getNameNode();\n let name = nameNode.getText();\n if (name.startsWith('\"') && name.endsWith('\"')) name = name.slice(1, -1);\n if (name.startsWith(\"'\") && name.endsWith(\"'\")) name = name.slice(1, -1);\n\n // If the JSON object doesn't have this key, check if we should preserve it\n if (!(name in record)) {\n const init = oldProp.getInitializer();\n if (init) {\n const kind = init.getKind();\n const isCode = kind === SyntaxKind.ArrowFunction ||\n kind === SyntaxKind.FunctionExpression ||\n kind === SyntaxKind.Identifier ||\n kind === SyntaxKind.CallExpression ||\n kind === SyntaxKind.JsxElement;\n\n if (isCode || name === \"target\" || name === \"callbacks\" || name === \"permissions\" || name === \"securityRules\") {\n // Preserve this property exactly as it was\n preservedProps.push(`${AstSchemaEditor.quoteKey(name)}: ${init.getText()}`);\n }\n }\n }\n }\n }\n }\n\n if (keys.length === 0 && preservedProps.length === 0) return \"{}\";\n\n const props = keys.map(key => {\n const keyStr = AstSchemaEditor.quoteKey(key);\n\n // If the value is an object, pass the old AST node to recurse\n let childAstNode: ObjectLiteralExpression | undefined;\n if (oldAstNode && AstSchemaEditor.isPlainObject(record[key])) {\n const oldProp = this.findProperty(oldAstNode, key);\n if (oldProp && oldProp.isKind(SyntaxKind.PropertyAssignment)) {\n childAstNode = oldProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n }\n }\n\n return `${keyStr}: ${this.convertJsonToAstString(record[key], indentLevel + 1, childAstNode)}`;\n });\n\n const allProps = [...props, ...preservedProps];\n return `{\\n${innerIndent}${allProps.join(`,\\n${innerIndent}`)}\\n${indent}}`;\n }\n return \"undefined\";\n }\n\n /**\n * Write only the keys the patch names, leaving every sibling alone.\n *\n * A patch says what changed, not what the collection is. The panel sends one\n * — `{ propertiesOrder }` is what adding a column posts — and rewriting the\n * `admin` block from it deleted the collection's icon, group, list columns\n * and kanban config in the same write.\n */\n private mergeIntoObjectLiteral(target: ObjectLiteralExpression, data: Record<string, unknown>, indentLevel: number): void {\n for (const [key, value] of Object.entries(data)) {\n const existing = this.findProperty(target, key);\n const existingObj = existing && existing.isKind(SyntaxKind.PropertyAssignment)\n ? existing.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression)\n : undefined;\n\n if (existingObj && AstSchemaEditor.isPlainObject(value)) {\n this.mergeIntoObjectLiteral(existingObj, value, indentLevel + 1);\n continue;\n }\n\n const initializer = this.convertJsonToAstString(value, indentLevel, existingObj);\n if (existing && existing.isKind(SyntaxKind.PropertyAssignment)) {\n existing.setInitializer(initializer);\n } else {\n target.addPropertyAssignment({\n name: AstSchemaEditor.quoteKey(key),\n initializer\n });\n }\n }\n }\n\n public async saveProperty(collectionId: string, propertyKey: string, propertyConfig: Record<string, unknown>) {\n const collectionObj = this.requireCollectionObject(collectionId);\n\n // The panel's property forms bind to the flat names — `readOnly`,\n // `hideFromCollection` — while on disk they belong inside the property's\n // own `admin` block. Written flat they are not merely ignored: the boot\n // validator treats a moved key as fatal.\n const nestedConfig = nestAdminPropertyKeys(propertyConfig);\n\n let propertiesProp = collectionObj.getProperty(\"properties\") as PropertyAssignment;\n if (!propertiesProp) {\n propertiesProp = collectionObj.addPropertyAssignment({\n name: \"properties\",\n initializer: \"{}\"\n });\n }\n\n const propsObj = propertiesProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n if (propsObj) {\n const existingProp = this.findProperty(propsObj, propertyKey);\n\n let oldPropAstNode: ObjectLiteralExpression | undefined;\n if (existingProp && existingProp.isKind(SyntaxKind.PropertyAssignment)) {\n oldPropAstNode = existingProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n }\n\n const newInitializer = this.convertJsonToAstString(nestedConfig, 2, oldPropAstNode);\n\n if (existingProp) {\n if (existingProp.isKind(SyntaxKind.PropertyAssignment)) {\n existingProp.setInitializer(newInitializer);\n }\n } else {\n propsObj.addPropertyAssignment({\n name: AstSchemaEditor.quoteKey(propertyKey),\n initializer: newInitializer\n });\n }\n\n const file = this.getCollectionFile(collectionId);\n if (file) {\n file.formatText();\n }\n await this.project.save();\n }\n }\n\n public async deleteProperty(collectionId: string, propertyKey: string) {\n const collectionObj = this.requireCollectionObject(collectionId);\n\n const propertiesProp = collectionObj.getProperty(\"properties\") as PropertyAssignment;\n if (propertiesProp) {\n const propsObj = propertiesProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n if (propsObj) {\n const existingProp = this.findProperty(propsObj, propertyKey);\n if (existingProp) {\n existingProp.remove();\n const file = this.getCollectionFile(collectionId);\n if (file) {\n file.formatText();\n }\n await this.project.save();\n }\n }\n }\n }\n\n /**\n * Write a collection back to its file.\n *\n * `partial` is the difference between \"this is the collection\" and \"this is\n * what changed about it\". The panel sends both — a full save from the editor\n * dialog, and a one-key patch whenever a property is added, deleted or\n * reordered — and they cannot be told apart by looking at the payload. Read\n * as a full save, a patch deletes everything it does not mention, including\n * `securityRules`; the loader then hands the collection the directory\n * default, which in the scaffold is `access: \"public\"`.\n */\n public async saveCollection(collectionId: string, collectionData: Record<string, unknown>, options: { partial?: boolean } = {}) {\n const partial = options.partial === true;\n let file = this.getCollectionFile(collectionId);\n const collectionObj = file ? this.getCollectionObject(collectionId) : null;\n\n if (file && !collectionObj) {\n // The file is there and we could not read it. Recreating it from the\n // panel's JSON would drop the imports, the callbacks and the relation\n // thunks that JSON cannot carry.\n throw new Error(this.unreadableFileMessage(collectionId, file));\n }\n\n if (!file || !collectionObj) {\n if (partial) {\n throw new Error(`Cannot apply a partial update to \"${collectionId}\": it has no collection file yet.`);\n }\n // Create a new file\n const safeId = this.sanitizeCollectionId(collectionId);\n const newFilePath = this.safePath(`${safeId}.ts`);\n if (fs.existsSync(newFilePath)) {\n throw new Error(`Refusing to overwrite ${newFilePath}: a file for \"${collectionId}\" already exists but could not be parsed.`);\n }\n file = this.project.createSourceFile(newFilePath, `import { CollectionConfig } from \"@rebasepro/types\";\\n\\nconst ${safeId}Collection: CollectionConfig = ${this.convertJsonToAstString(nestAdminKeys(collectionData))};\\n\\nexport default ${safeId}Collection;\\n`);\n } else {\n // Update root level properties gracefully\n\n if (!partial) {\n // Force delete securityRules if empty or undefined to handle Formex / serialization stripping\n if (!(\"securityRules\" in collectionData) || collectionData.securityRules === undefined || (Array.isArray(collectionData.securityRules) && collectionData.securityRules.length === 0)) {\n const srProp = collectionObj.getProperty(\"securityRules\");\n if (srProp) {\n srProp.remove();\n }\n\n // If it was in collectionData as an empty array, delete it so the loop below doesn't add it back as \"[]\"\n // Actually, if it's \"[]\", omitting it entirely from the TS file achieves the same logical effect (no RLS rules)\n // and correctly triggers \"unmapped policies\" if the DB still has them.\n delete collectionData[\"securityRules\"];\n }\n }\n\n // The panel works with a flat view model — presentation merged onto the\n // collection — so what arrives here has `icon` and `listProperties` at\n // the top level. On disk they belong inside `admin`. Writing them flat\n // would produce a file the backend loads and ignores and the panel\n // never reads back, which looks exactly like the edit not saving.\n collectionData = nestAdminKeys(collectionData);\n\n for (const key of Object.keys(collectionData)) {\n if (key === \"relations\") {\n this.writeRelations(collectionId, file, collectionObj, collectionData[key]);\n continue;\n }\n\n const prop = collectionObj.getProperty(key) as PropertyAssignment;\n\n let oldAstNode: ObjectLiteralExpression | undefined;\n if (prop && prop.isKind(SyntaxKind.PropertyAssignment)) {\n oldAstNode = prop.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n }\n\n if (partial && oldAstNode && AstSchemaEditor.isPlainObject(collectionData[key])) {\n this.mergeIntoObjectLiteral(oldAstNode, collectionData[key] as Record<string, unknown>, 2);\n continue;\n }\n\n const newInit = this.convertJsonToAstString(collectionData[key], 1, oldAstNode);\n if (prop) {\n prop.setInitializer(newInit);\n } else {\n collectionObj.addPropertyAssignment({\n name: key,\n initializer: newInit\n });\n }\n }\n }\n if (file) {\n file.formatText();\n }\n await this.project.save();\n }\n\n /**\n * Write the collection-level `relations` array.\n *\n * Relations are the one key whose values are not all data: `target` is a\n * thunk in the file, and the panel sends the target collection's slug (or,\n * for a relation it did not touch, nothing at all — `JSON.stringify` drops\n * the function). So each entry's target is resolved in that order: a slug\n * becomes `() => xCollection` plus the import it needs, and anything else\n * falls back to the thunk already in the file.\n *\n * This used to be a `continue` with a comment saying relations were handled\n * elsewhere. Nothing handled them; every edit in the Relations tab was\n * dropped without a word.\n */\n private writeRelations(collectionId: string, file: SourceFile, collectionObj: ObjectLiteralExpression, value: unknown): void {\n if (!Array.isArray(value)) return;\n\n const existing = this.findProperty(collectionObj, \"relations\");\n const oldArray = existing && existing.isKind(SyntaxKind.PropertyAssignment)\n ? existing.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)\n : undefined;\n\n const oldElements = oldArray?.getElements() ?? [];\n const oldTargetsByName = new Map<string, string>();\n // Only for entries the file left unnamed — `relationName` is optional, and\n // those cannot be matched any other way. Positional matching is off the\n // moment the array's length changes, because then a position no longer\n // means the same relation.\n const anonymousTargetsByIndex: (string | undefined)[] = [];\n oldElements.forEach((element, index) => {\n const elementObj = element.asKind(SyntaxKind.ObjectLiteralExpression);\n if (!elementObj) return;\n const targetProp = this.findProperty(elementObj, \"target\");\n const targetInit = targetProp && targetProp.isKind(SyntaxKind.PropertyAssignment)\n ? targetProp.getInitializer()\n : undefined;\n if (!targetInit) return;\n\n const nameProp = this.findProperty(elementObj, \"relationName\");\n const nameInit = nameProp && nameProp.isKind(SyntaxKind.PropertyAssignment)\n ? nameProp.getInitializerIfKind(SyntaxKind.StringLiteral)\n : undefined;\n if (nameInit) oldTargetsByName.set(nameInit.getLiteralValue(), targetInit.getText());\n else anonymousTargetsByIndex[index] = targetInit.getText();\n });\n\n const items = value.map((entry, index) => {\n if (!AstSchemaEditor.isPlainObject(entry)) return entry;\n const relation: Record<string, unknown> = { ...entry };\n const relationName = typeof relation.relationName === \"string\" ? relation.relationName : undefined;\n const rawTarget = relation.target;\n\n let targetText: string | undefined;\n if (typeof rawTarget === \"string\" && rawTarget.trim().length > 0) {\n targetText = rawTarget.includes(\"=>\")\n ? rawTarget.trim()\n : this.targetThunk(file, rawTarget.trim());\n } else if (relationName && oldTargetsByName.has(relationName)) {\n targetText = oldTargetsByName.get(relationName);\n } else if (value.length === oldElements.length) {\n targetText = anonymousTargetsByIndex[index];\n }\n\n if (!targetText) {\n throw new Error(`Relation \"${relationName ?? `#${index}`}\" on collection \"${collectionId}\" has no target collection. Pick one before saving.`);\n }\n\n relation.target = new RawExpression(targetText);\n return relation;\n });\n\n const initializer = this.convertJsonToAstString(items, 1);\n if (existing && existing.isKind(SyntaxKind.PropertyAssignment)) {\n existing.setInitializer(initializer);\n } else {\n collectionObj.addPropertyAssignment({ name: \"relations\", initializer });\n }\n }\n\n /**\n * `() => targetCollection` for a target named by its slug, importing it if\n * the file does not already.\n */\n private targetThunk(file: SourceFile, targetSlug: string): string {\n const targetFile = this.getCollectionFile(targetSlug);\n if (!targetFile) {\n throw new Error(`Cannot link to collection \"${targetSlug}\": no file for it in ${this.collectionsDir}.`);\n }\n\n const identifier = this.getDefaultExportName(targetFile);\n if (!identifier) {\n throw new Error(`Cannot link to collection \"${targetSlug}\": ${targetFile.getFilePath()} has no default export to import.`);\n }\n\n if (targetFile.getFilePath() !== file.getFilePath() && !this.hasDefaultImport(file, identifier)) {\n const relative = path.relative(path.dirname(file.getFilePath()), targetFile.getFilePath())\n .split(path.sep)\n .join(\"/\")\n .replace(/\\.tsx?$/, \".js\");\n file.addImportDeclaration({\n defaultImport: identifier,\n moduleSpecifier: relative.startsWith(\".\") ? relative : `./${relative}`\n });\n }\n\n return `() => ${identifier}`;\n }\n\n private hasDefaultImport(file: SourceFile, identifier: string): boolean {\n return file.getImportDeclarations().some(decl => decl.getDefaultImport()?.getText() === identifier);\n }\n\n private getDefaultExportName(file: SourceFile): string | undefined {\n const declaration = file.getDefaultExportSymbol()?.getDeclarations()[0];\n const expr = declaration?.asKind(SyntaxKind.ExportAssignment)?.getExpression();\n if (expr && expr.getKind() === SyntaxKind.Identifier) return expr.getText();\n\n for (const varDecl of file.getVariableDeclarations()) {\n if (this.unwrapCollectionObject(varDecl.getInitializer())) return varDecl.getName();\n }\n return undefined;\n }\n\n public async deleteCollection(collectionId: string) {\n const file = this.getCollectionFile(collectionId);\n if (file) {\n file.deleteImmediatelySync();\n }\n }\n}\n","import { Hono } from \"hono\";\nimport { AstSchemaEditor } from \"./ast-schema-editor\";\nimport { errorHandler } from \"./errors\";\nimport { HonoEnv } from \"./types\";\n\nexport function createSchemaEditorRoutes(collectionsDir: string): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n const editor = new AstSchemaEditor(collectionsDir);\n\n router.post(\"/property/save\", async (c) => {\n const body = await c.req.json();\n const { collectionId, propertyKey, propertyConfig } = body;\n await editor.saveProperty(collectionId, propertyKey, propertyConfig);\n return c.json({ success: true });\n });\n\n router.post(\"/property/delete\", async (c) => {\n const body = await c.req.json();\n const { collectionId, propertyKey } = body;\n await editor.deleteProperty(collectionId, propertyKey);\n return c.json({ success: true });\n });\n\n /**\n * `partial: true` means \"this payload is what changed\", not \"this is the\n * collection\". Without it a one-key patch — which is what adding a column\n * posts — is read as a whole-collection save and deletes everything it does\n * not mention, `securityRules` included. Absent, it defaults to a full save,\n * so an older panel keeps the behaviour it was written against.\n */\n router.post(\"/collection/save\", async (c) => {\n const body = await c.req.json();\n const { collectionId, collectionData, partial } = body;\n await editor.saveCollection(collectionId, collectionData, { partial: partial === true });\n return c.json({ success: true });\n });\n\n router.post(\"/collection/delete\", async (c) => {\n const body = await c.req.json();\n const { collectionId } = body;\n await editor.deleteCollection(collectionId);\n return c.json({ success: true });\n });\n\n return router;\n}\n\n"],"mappings":"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"}
|