@rebasepro/server 0.16.0 → 0.16.1-canary.g701140a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/dist/api/contract-routes.d.ts +1 -1
  2. package/dist/api/errors.d.ts +1 -1
  3. package/dist/api/index.d.ts +3 -3
  4. package/dist/api/logs-routes.d.ts +1 -1
  5. package/dist/api/rest/api-generator.d.ts +2 -2
  6. package/dist/api/rest/index.d.ts +1 -1
  7. package/dist/api/rest/query-parser.d.ts +1 -1
  8. package/dist/api/schema-editor-routes.d.ts +1 -1
  9. package/dist/api/types.d.ts +3 -4
  10. package/dist/auth/adapter-middleware.d.ts +2 -2
  11. package/dist/auth/admin-roles-route.d.ts +2 -2
  12. package/dist/auth/admin-user-ops.d.ts +3 -3
  13. package/dist/auth/admin-users-route.d.ts +4 -4
  14. package/dist/auth/api-keys/api-key-middleware.d.ts +2 -2
  15. package/dist/auth/api-keys/api-key-permission-guard.d.ts +1 -1
  16. package/dist/auth/api-keys/api-key-routes.d.ts +2 -2
  17. package/dist/auth/api-keys/api-key-store.d.ts +1 -1
  18. package/dist/auth/api-keys/index.d.ts +9 -9
  19. package/dist/auth/apple-oauth.d.ts +2 -2
  20. package/dist/auth/auth-hooks.d.ts +3 -3
  21. package/dist/auth/bitbucket-oauth.d.ts +2 -2
  22. package/dist/auth/builtin-auth-adapter.d.ts +7 -4
  23. package/dist/auth/captcha.d.ts +86 -0
  24. package/dist/auth/cookie-utils.d.ts +2 -2
  25. package/dist/auth/discord-oauth.d.ts +2 -2
  26. package/dist/auth/facebook-oauth.d.ts +2 -2
  27. package/dist/auth/github-oauth.d.ts +2 -2
  28. package/dist/auth/gitlab-oauth.d.ts +2 -2
  29. package/dist/auth/google-oauth.d.ts +1 -1
  30. package/dist/auth/index.d.ts +56 -54
  31. package/dist/auth/jwks-routes.d.ts +1 -1
  32. package/dist/auth/jwt.d.ts +1 -1
  33. package/dist/auth/linkedin-oauth.d.ts +2 -2
  34. package/dist/auth/magic-link-routes.d.ts +10 -3
  35. package/dist/auth/mfa-gate.d.ts +1 -1
  36. package/dist/auth/mfa-routes.d.ts +3 -3
  37. package/dist/auth/microsoft-oauth.d.ts +2 -2
  38. package/dist/auth/middleware.d.ts +5 -5
  39. package/dist/auth/rate-limiter.d.ts +2 -2
  40. package/dist/auth/require-auth.d.ts +1 -1
  41. package/dist/auth/reset-password-admin.d.ts +4 -4
  42. package/dist/auth/routes.d.ts +13 -4
  43. package/dist/auth/session-routes.d.ts +3 -3
  44. package/dist/auth/slack-oauth.d.ts +2 -2
  45. package/dist/auth/spotify-oauth.d.ts +2 -2
  46. package/dist/auth/token-revocation.d.ts +2 -2
  47. package/dist/auth/twitter-oauth.d.ts +2 -2
  48. package/dist/{auth-5Et5mnUA.js → auth-B0IV-irB.js} +250 -15
  49. package/dist/auth-B0IV-irB.js.map +1 -0
  50. package/dist/backup/backup-common.d.ts +1 -1
  51. package/dist/backup/backup-routes.d.ts +3 -3
  52. package/dist/backup/index.d.ts +3 -3
  53. package/dist/{backup-C6ljYVTp.js → backup-BJ86ah4T.js} +2 -2
  54. package/dist/{backup-C6ljYVTp.js.map → backup-BJ86ah4T.js.map} +1 -1
  55. package/dist/boot/boot.d.ts +5 -5
  56. package/dist/boot/bundle.d.ts +1 -1
  57. package/dist/boot/driver.d.ts +1 -1
  58. package/dist/boot/env.d.ts +13 -1
  59. package/dist/boot/options.d.ts +30 -6
  60. package/dist/boot/role.d.ts +3 -2
  61. package/dist/boot/sources.d.ts +1 -1
  62. package/dist/collections/BackendCollectionRegistry.d.ts +1 -1
  63. package/dist/collections/loader.d.ts +1 -1
  64. package/dist/{contract-routes-DZ-LBpSL.js → contract-routes-BEq7euZg.js} +2 -2
  65. package/dist/contract-routes-BEq7euZg.js.map +1 -0
  66. package/dist/cron/cron-routes.d.ts +2 -2
  67. package/dist/cron/cron-scheduler.d.ts +2 -2
  68. package/dist/cron/index.d.ts +8 -8
  69. package/dist/{cron-loader-YhhQeVBM.js → cron-loader-BMvtW6-J.js} +2 -2
  70. package/dist/{cron-loader-YhhQeVBM.js.map → cron-loader-BMvtW6-J.js.map} +1 -1
  71. package/dist/{cron-routes-maM_RlUu.js → cron-routes-BvYk-Kmi.js} +2 -2
  72. package/dist/{cron-routes-maM_RlUu.js.map → cron-routes-BvYk-Kmi.js.map} +1 -1
  73. package/dist/{cron-scheduler-DIpYBmZP.js → cron-scheduler-Buf-uVam.js} +2 -2
  74. package/dist/{cron-scheduler-DIpYBmZP.js.map → cron-scheduler-Buf-uVam.js.map} +1 -1
  75. package/dist/{cron-store-DfH_4Cd9.js → cron-store-CpfttP4f.js} +3 -3
  76. package/dist/{cron-store-DfH_4Cd9.js.map → cron-store-CpfttP4f.js.map} +1 -1
  77. package/dist/{ddl-bootstrap-Cywoj8Ta.js → ddl-bootstrap-CFLhcvfQ.js} +2 -2
  78. package/dist/{ddl-bootstrap-Cywoj8Ta.js.map → ddl-bootstrap-CFLhcvfQ.js.map} +1 -1
  79. package/dist/dev-secrets.d.ts +52 -0
  80. package/dist/email/dev-sink.d.ts +67 -0
  81. package/dist/email/index.d.ts +9 -7
  82. package/dist/email/link-base.d.ts +1 -1
  83. package/dist/email/smtp-email-service.d.ts +1 -1
  84. package/dist/env.d.ts +1 -1
  85. package/dist/{errors-EBYiaJ2E.js → errors-D_LwNKRM.js} +5 -5
  86. package/dist/errors-D_LwNKRM.js.map +1 -0
  87. package/dist/{function-loader-DDS1v7YX.js → function-loader-BrLmC_-y.js} +3 -3
  88. package/dist/{function-loader-DDS1v7YX.js.map → function-loader-BrLmC_-y.js.map} +1 -1
  89. package/dist/functions/context.d.ts +141 -0
  90. package/dist/functions/define-function.d.ts +1 -1
  91. package/dist/functions/function-routes.d.ts +2 -2
  92. package/dist/functions/guards.d.ts +76 -0
  93. package/dist/functions/index.d.ts +96 -5
  94. package/dist/functions/index.js +919 -0
  95. package/dist/functions/index.js.map +1 -0
  96. package/dist/functions/internal.d.ts +26 -0
  97. package/dist/functions/proxy.d.ts +1 -1
  98. package/dist/functions/request-timeout.d.ts +9 -1
  99. package/dist/functions/runtime-env.d.ts +92 -0
  100. package/dist/functions/wait-until.d.ts +74 -0
  101. package/dist/history/history-routes.d.ts +2 -2
  102. package/dist/history/index.d.ts +1 -1
  103. package/dist/index.d.ts +63 -54
  104. package/dist/index.es.js +1126 -53
  105. package/dist/index.es.js.map +1 -1
  106. package/dist/init/docs.d.ts +1 -1
  107. package/dist/init/middlewares.d.ts +1 -1
  108. package/dist/init/shutdown.d.ts +4 -0
  109. package/dist/init/storage.d.ts +1 -1
  110. package/dist/init/surfaces.d.ts +10 -0
  111. package/dist/init.d.ts +72 -18
  112. package/dist/jobs/index.d.ts +5 -5
  113. package/dist/{jobs-CyOKXXlu.js → jobs-CSlBZ8dn.js} +3 -3
  114. package/dist/{jobs-CyOKXXlu.js.map → jobs-CSlBZ8dn.js.map} +1 -1
  115. package/dist/{jwt-DxH9fLPt.js → jwt-BbJi0TR0.js} +2 -2
  116. package/dist/{jwt-DxH9fLPt.js.map → jwt-BbJi0TR0.js.map} +1 -1
  117. package/dist/{logger-DfvF_8r-.js → logger-TdvXIGqR.js} +101 -8
  118. package/dist/logger-TdvXIGqR.js.map +1 -0
  119. package/dist/metrics/index.d.ts +1 -1
  120. package/dist/{proxy-Bj5DVllb.js → proxy-QJKSS-CV.js} +5 -3
  121. package/dist/{proxy-Bj5DVllb.js.map → proxy-QJKSS-CV.js.map} +1 -1
  122. package/dist/{request-timeout-BuFoEKwT.js → request-timeout-OofPCHQT.js} +17 -3
  123. package/dist/request-timeout-OofPCHQT.js.map +1 -0
  124. package/dist/rls-audit/index.d.ts +111 -0
  125. package/dist/{schema-editor-routes-CV9k0w3G.js → schema-editor-routes-Cax2XTRj.js} +2 -2
  126. package/dist/{schema-editor-routes-CV9k0w3G.js.map → schema-editor-routes-Cax2XTRj.js.map} +1 -1
  127. package/dist/services/webhook-service.d.ts +1 -1
  128. package/dist/singleton.d.ts +7 -0
  129. package/dist/storage/GCSStorageController.d.ts +1 -1
  130. package/dist/storage/LocalStorageController.d.ts +1 -1
  131. package/dist/storage/S3StorageController.d.ts +1 -1
  132. package/dist/storage/cache-headers.d.ts +87 -0
  133. package/dist/storage/index.d.ts +11 -11
  134. package/dist/storage/policies.d.ts +88 -0
  135. package/dist/storage/range.d.ts +63 -0
  136. package/dist/storage/routes.d.ts +3 -3
  137. package/dist/storage/storage-registry.d.ts +1 -1
  138. package/dist/storage/tus-handler.d.ts +2 -2
  139. package/dist/utils/host.d.ts +58 -0
  140. package/dist/utils/logger.d.ts +0 -15
  141. package/dist/utils/request-id.d.ts +1 -1
  142. package/functions/package.json +24 -0
  143. package/package.json +13 -7
  144. package/dist/auth-5Et5mnUA.js.map +0 -1
  145. package/dist/contract-routes-DZ-LBpSL.js.map +0 -1
  146. package/dist/errors-EBYiaJ2E.js.map +0 -1
  147. package/dist/logger-DfvF_8r-.js.map +0 -1
  148. package/dist/request-timeout-BuFoEKwT.js.map +0 -1
@@ -1 +1 @@
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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"}
1
+ {"version":3,"file":"schema-editor-routes-Cax2XTRj.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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"}
@@ -1,4 +1,4 @@
1
- import type { JobQueueClient } from "../jobs/types";
1
+ import type { JobQueueClient } from "../jobs/types.js";
2
2
  /**
3
3
  * The task name a queued webhook delivery is stored under.
4
4
  *
@@ -1,4 +1,11 @@
1
1
  import type { RebaseServerClient } from "@rebasepro/types";
2
+ /**
3
+ * @internal Register the per-call resolver described on {@link RESOLVER_SLOT}.
4
+ *
5
+ * For runtime adapters, not for application code. Pass `null` to unregister.
6
+ * Returns the previous resolver so an adapter can restore it.
7
+ */
8
+ export declare function _setRebaseResolver(resolve: (() => RebaseServerClient | null) | null): (() => RebaseServerClient | null) | null;
2
9
  /**
3
10
  * @internal Called once during server initialization to set the backing instance.
4
11
  * This is invoked by `initializeRebaseBackend()` — never call it manually.
@@ -5,7 +5,7 @@
5
5
  * optional peer dependency — users who don't need GCS never pay for the
6
6
  * import cost or need it installed.
7
7
  */
8
- import { StorageController, GCSStorageConfig } from "./types";
8
+ import { StorageController, GCSStorageConfig } from "./types.js";
9
9
  import { UploadFileProps, UploadFileResult, DownloadConfig, StorageListResult } from "@rebasepro/types";
10
10
  /**
11
11
  * Google Cloud Storage implementation of `StorageController`.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Local filesystem storage controller
3
3
  */
4
- import { StorageController, LocalStorageConfig } from "./types";
4
+ import { StorageController, LocalStorageConfig } from "./types.js";
5
5
  import { UploadFileProps, UploadFileResult, DownloadConfig, StorageListResult } from "@rebasepro/types";
6
6
  /**
7
7
  * Bucket used when a call names none.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * S3-compatible storage controller (works with AWS S3 and MinIO)
3
3
  */
4
- import { S3StorageConfig, StorageController } from "./types";
4
+ import { S3StorageConfig, StorageController } from "./types.js";
5
5
  import { DownloadConfig, StorageListResult, UploadFileProps, UploadFileResult } from "@rebasepro/types";
6
6
  /**
7
7
  * S3-compatible storage implementation
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Cache and revalidation headers for served objects.
3
+ *
4
+ * ## Why every byte still goes through the process
5
+ *
6
+ * `routes.ts` deliberately proxies rather than redirecting to a signed URL —
7
+ * mixed content and unreachable VPC endpoints, both documented there. That
8
+ * decision stands. What it cost was caching: a proxied response only caches as
9
+ * well as its headers say, and the local path sent none at all, so every load
10
+ * of every image re-read the file and re-sent the body.
11
+ *
12
+ * This module is the headers. It does not change where bytes come from; it
13
+ * lets the client stop asking for them.
14
+ *
15
+ * ## Why `immutable` is wrong here, and was
16
+ *
17
+ * The remote path sent `public, max-age=3600, immutable`, and the transform
18
+ * paths sent it with `max-age=31536000`. `immutable` is a promise that the body
19
+ * at this URL will never change. A storage key can be overwritten — `putObject`
20
+ * on an existing key is an ordinary operation — so the promise is false for
21
+ * every object this server serves, and the symptom is an upload that replaces a
22
+ * file which then does not visibly change for an hour, or a year.
23
+ *
24
+ * Revalidation replaces it. With a validator the client asks "still this one?"
25
+ * and gets a 304 with no body, which costs a round trip instead of a transfer.
26
+ *
27
+ * ## Why `public` was a leak
28
+ *
29
+ * The same header said `public` for every object, including one that required
30
+ * authentication to fetch. `public` is explicit permission for a *shared* cache
31
+ * — a CDN, a corporate proxy — to store the response and serve it to somebody
32
+ * else. Private objects are now `private`, which is the directive that says a
33
+ * browser may keep it but a shared cache may not.
34
+ */
35
+ import type { Context } from "hono";
36
+ /**
37
+ * A weak validator built from size and modification time.
38
+ *
39
+ * Weak (`W/`) because it is not a digest of the body: two different bodies of
40
+ * the same length written in the same millisecond would collide. That is the
41
+ * same trade nginx makes for static files, and the alternative — hashing every
42
+ * object on every request — costs more than it saves for content this server is
43
+ * already streaming from somewhere else.
44
+ */
45
+ export declare function buildEntityTag(size: number, lastModifiedMs: number): string;
46
+ export interface ObjectValidators {
47
+ etag: string;
48
+ /** RFC 9110 date, second-resolution. */
49
+ lastModified: string;
50
+ lastModifiedMs: number;
51
+ }
52
+ export declare function objectValidators(size: number, lastModifiedMs: number): ObjectValidators;
53
+ export interface CacheControlOptions {
54
+ /**
55
+ * Whether a *shared* cache may store this. True only for objects that need
56
+ * no credentials to fetch.
57
+ */
58
+ isPublic: boolean;
59
+ /**
60
+ * How long a client may reuse the response without asking. Short by
61
+ * default, because the object behind it is mutable.
62
+ */
63
+ maxAgeSeconds: number;
64
+ /**
65
+ * Public responses only: how long a stale copy may be served while a
66
+ * revalidation happens in the background. Shared caches honour it; it turns
67
+ * the revalidation round trip into something the user does not wait for.
68
+ */
69
+ staleWhileRevalidateSeconds?: number;
70
+ }
71
+ export declare function cacheControl(options: CacheControlOptions): string;
72
+ /**
73
+ * Whether the request already holds this exact representation.
74
+ *
75
+ * `If-None-Match` wins outright when present: it is an exact validator, whereas
76
+ * `If-Modified-Since` has second resolution and is only consulted in its
77
+ * absence — again the order RFC 9110 requires.
78
+ */
79
+ export declare function isNotModified(c: Context, validators: ObjectValidators): boolean;
80
+ /**
81
+ * Put the validators and the cache policy on a response.
82
+ *
83
+ * Applied to the 304 as well as the 200: a 304 that omits them tells the client
84
+ * nothing about how long the answer holds, so the next load revalidates again
85
+ * immediately and the round trip repeats forever.
86
+ */
87
+ export declare function applyCacheHeaders(c: Context, validators: ObjectValidators, options: CacheControlOptions): void;
@@ -11,17 +11,17 @@
11
11
  * For other providers (Azure Blob, etc.), implement the
12
12
  * `StorageController` interface and pass the instance directly to the `storage` config.
13
13
  */
14
- export * from "./types";
15
- export { LocalStorageController } from "./LocalStorageController";
16
- export { S3StorageController } from "./S3StorageController";
17
- export { GCSStorageController } from "./GCSStorageController";
18
- export { createStorageRoutes } from "./routes";
19
- export type { StorageRoutesConfig } from "./routes";
20
- export * from "./storage-registry";
21
- export { parseTransformOptions, transformImage, isTransformableImage, TransformCache } from "./image-transform";
22
- export type { ImageTransformOptions } from "./image-transform";
23
- export { TusHandler } from "./tus-handler";
24
- import { BackendStorageConfig, StorageController } from "./types";
14
+ export * from "./types.js";
15
+ export { LocalStorageController } from "./LocalStorageController.js";
16
+ export { S3StorageController } from "./S3StorageController.js";
17
+ export { GCSStorageController } from "./GCSStorageController.js";
18
+ export { createStorageRoutes } from "./routes.js";
19
+ export type { StorageRoutesConfig } from "./routes.js";
20
+ export * from "./storage-registry.js";
21
+ export { parseTransformOptions, transformImage, isTransformableImage, TransformCache } from "./image-transform.js";
22
+ export type { ImageTransformOptions } from "./image-transform.js";
23
+ export { TusHandler } from "./tus-handler.js";
24
+ import { BackendStorageConfig, StorageController } from "./types.js";
25
25
  /**
26
26
  * Create a storage controller from a config object.
27
27
  *
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Declarative access control for storage.
3
+ *
4
+ * Storage is not under row-level security, so `storageAuthorize` — a hook, a
5
+ * piece of imperative code — was the whole access-control model. That is the one
6
+ * place the claim "security lives in the database" does not hold, and it shows
7
+ * in the shape of the alternative: without a hook, **any authenticated caller
8
+ * may read, overwrite, delete or list any key they can name**, because storage
9
+ * keys share one flat namespace.
10
+ *
11
+ * A policy list says the same things declaratively, and can be read without
12
+ * running it:
13
+ *
14
+ * ```ts
15
+ * storagePolicies: [
16
+ * { path: "public/**", operations: ["read"], allow: "public" },
17
+ * { path: "users/:uid/**", allow: ({ params, user }) => user?.uid === params.uid }
18
+ * ]
19
+ * ```
20
+ *
21
+ * ## Deny by default, and why that is the whole design
22
+ *
23
+ * A key matched by no policy is refused. That makes every widening an explicit,
24
+ * reviewable line, and it makes a mistake in this module fail *closed* — a
25
+ * pattern that fails to match denies access rather than granting it. Authz code
26
+ * that fails open is worth more to an attacker than the feature is to anyone
27
+ * else, so the bias is deliberate and absolute.
28
+ *
29
+ * ## The hook remains, as an escape hatch and not as a fallback
30
+ *
31
+ * Ownership frequently lives in a row rather than in a key, which no pattern can
32
+ * express. So when policies match nothing, an explicit `storageAuthorize` is
33
+ * consulted before the request is refused. Present, it can widen; absent, the
34
+ * answer is no. Policies never *narrow* what the hook allowed and the hook never
35
+ * overrides a policy denial — each can only say yes, which keeps "who allowed
36
+ * this?" answerable by reading either one alone.
37
+ */
38
+ import type { StorageAuthorize, StorageAuthorizeContext, StorageOperation } from "@rebasepro/types";
39
+ /** Everything a predicate is told, plus whatever the path captured. */
40
+ export interface StoragePolicyContext extends StorageAuthorizeContext {
41
+ /** Segments captured by `:name` placeholders in the matched path. */
42
+ params: Record<string, string>;
43
+ }
44
+ export type StoragePolicyPredicate = (ctx: StoragePolicyContext) => boolean | Promise<boolean>;
45
+ export interface StoragePolicy {
46
+ /**
47
+ * A key pattern, matched segment by segment against the sanitized key.
48
+ *
49
+ * - a literal segment matches itself
50
+ * - `*` matches exactly one segment
51
+ * - `:name` matches one segment and captures it
52
+ * - `**` matches the rest of the key, including nothing, and is only
53
+ * allowed as the final segment
54
+ */
55
+ path: string;
56
+ /** Which operations this grants. Defaults to every one of them. */
57
+ operations?: StorageOperation[];
58
+ /**
59
+ * Who it grants to.
60
+ *
61
+ * - `"public"` — anyone, signed in or not
62
+ * - `"authenticated"` — any caller with a uid
63
+ * - a predicate — anything else, given the captured params and the user
64
+ */
65
+ allow: "public" | "authenticated" | StoragePolicyPredicate;
66
+ }
67
+ export declare class StoragePolicyError extends Error {
68
+ constructor(message: string);
69
+ }
70
+ /**
71
+ * Compile a policy list into a {@link StorageAuthorize}.
72
+ *
73
+ * Compiled once, at boot, so a malformed pattern fails the start rather than the
74
+ * first upload. `fallback` is an explicit `storageAuthorize`, consulted only
75
+ * when no policy matched.
76
+ */
77
+ export declare function compileStoragePolicies(policies: StoragePolicy[], fallback?: StorageAuthorize): StorageAuthorize;
78
+ /**
79
+ * The access-control function for a configuration, or `undefined` when it
80
+ * declares none.
81
+ *
82
+ * Kept separate from {@link compileStoragePolicies} so `init` has one call to
83
+ * make and one thing to reason about: policies, a hook, both, or neither.
84
+ */
85
+ export declare function resolveStorageAccessControl(config: {
86
+ storagePolicies?: StoragePolicy[];
87
+ storageAuthorize?: StorageAuthorize;
88
+ }): StorageAuthorize | undefined;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Byte-range requests for served objects.
3
+ *
4
+ * Without `Accept-Ranges`, a browser will not offer to seek in an audio or
5
+ * video element served from here: it has no way to ask for the middle of a file,
6
+ * so scrubbing means fetching the whole thing from the start, and on a long
7
+ * recording it means fetching it again for every seek. Safari goes further and
8
+ * refuses to play a `<video>` at all unless the first response is a `206`.
9
+ *
10
+ * So this is not a performance nicety. For media it is the difference between a
11
+ * player that works and one that does not.
12
+ *
13
+ * ## What is implemented, and what is refused
14
+ *
15
+ * A single range — `bytes=0-499`, `bytes=500-`, `bytes=-500`. That is what every
16
+ * browser sends for media playback.
17
+ *
18
+ * Multiple ranges in one request (`bytes=0-99,200-299`) are **not** served.
19
+ * Answering them means composing a `multipart/byteranges` body, and nothing that
20
+ * matters sends them; a server may always answer a range request with the whole
21
+ * `200` instead, which is what happens here. Refusing to guess is better than a
22
+ * half-implemented multipart encoder.
23
+ *
24
+ * An unsatisfiable range — one starting past the end of the object — is a `416`
25
+ * with a `Content-Range: bytes /<size>`, as RFC 9110 requires. Answering `200`
26
+ * there would hand a player the whole file when it asked for a byte that does
27
+ * not exist, and it would never notice.
28
+ */
29
+ /** A resolved, inclusive byte range within an object of known size. */
30
+ export interface ByteRange {
31
+ start: number;
32
+ /** Inclusive, as the header is. */
33
+ end: number;
34
+ /** `end - start + 1`. */
35
+ length: number;
36
+ }
37
+ export type RangeParse =
38
+ /** No `Range` header, or one this server declines to honour. Serve `200`. */
39
+ {
40
+ kind: "none";
41
+ }
42
+ /** Serve `206` with this range. */
43
+ | {
44
+ kind: "range";
45
+ range: ByteRange;
46
+ }
47
+ /** Serve `416`. The range cannot be satisfied for an object of this size. */
48
+ | {
49
+ kind: "unsatisfiable";
50
+ };
51
+ /**
52
+ * Parse a `Range` header against an object of `size` bytes.
53
+ *
54
+ * Deliberately conservative: anything malformed, multi-range, or in a unit other
55
+ * than bytes resolves to `none`, which serves the whole object. A range request
56
+ * answered with `200` is always legal; a range answered wrongly is a corrupted
57
+ * download.
58
+ */
59
+ export declare function parseRange(header: string | undefined, size: number): RangeParse;
60
+ /** The `Content-Range` for a served range. */
61
+ export declare function contentRange(range: ByteRange, size: number): string;
62
+ /** The `Content-Range` for a `416`, which names the size and no range. */
63
+ export declare function unsatisfiableContentRange(size: number): string;
@@ -7,10 +7,10 @@
7
7
  * is used.
8
8
  */
9
9
  import { Hono } from "hono";
10
- import { StorageController, type StorageAuthorize, type StorageAuthorizeData } from "./types";
11
- import { type StorageRegistry } from "./storage-registry";
10
+ import { StorageController, type StorageAuthorize, type StorageAuthorizeData } from "./types.js";
11
+ import { type StorageRegistry } from "./storage-registry.js";
12
12
  import { type StorageSourceDefinition, type AuthAdapter } from "@rebasepro/types";
13
- import { HonoEnv } from "../api/types";
13
+ import { HonoEnv } from "../api/types.js";
14
14
  /**
15
15
  * Decide what to actually serve for a stored content type.
16
16
  *
@@ -10,7 +10,7 @@
10
10
  * - String properties use `storageId` in their config to specify which storage to use
11
11
  * - Properties without `storageId` fallback to "(default)"
12
12
  */
13
- import { StorageController } from "./types";
13
+ import { StorageController } from "./types.js";
14
14
  /**
15
15
  * The default storage identifier used when:
16
16
  * - A single storage controller is provided (not a map)
@@ -8,8 +8,8 @@
8
8
  * @see https://tus.io/protocols/resumable-upload
9
9
  */
10
10
  import type { Context } from "hono";
11
- import type { StorageController } from "./types";
12
- import { type StorageRegistry } from "./storage-registry";
11
+ import type { StorageController } from "./types.js";
12
+ import { type StorageRegistry } from "./storage-registry.js";
13
13
  /**
14
14
  * TUS resumable upload handler.
15
15
  *
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The host globals this package reads, behind functions that do not assume Node.
3
+ *
4
+ * `process` is not defined on workerd, on Deno Deploy without the compat flag,
5
+ * or in a browser. That matters here for one specific reason: the portable
6
+ * authoring surface (`@rebasepro/server/functions`) reaches the logger and the
7
+ * error handler, and a bare `process.env.NODE_ENV` inside either of them turns
8
+ * the first log line of a request into a `ReferenceError` on a runtime that has
9
+ * no `process` — a failure that reads as "the framework crashed" rather than
10
+ * "this runtime has no process object".
11
+ *
12
+ * Nothing here throws and nothing here is async. A runtime that cannot answer
13
+ * gets the empty answer, because every caller in this file's blast radius is
14
+ * choosing a log level or a format, and the safe default for both is the
15
+ * development one.
16
+ *
17
+ * @module
18
+ */
19
+ /**
20
+ * The process environment, or the closest thing this runtime has to one.
21
+ *
22
+ * Order: a bag published by {@link setHostEnv} first, because an adapter that
23
+ * set one knows more than the ambient globals do; then `process.env`; then
24
+ * nothing.
25
+ */
26
+ export declare function hostEnv(): Record<string, string | undefined>;
27
+ /**
28
+ * Read one environment variable without touching `process` directly.
29
+ *
30
+ * Trimmed, and blank is treated as absent — a variable declared with no value
31
+ * is the ordinary way to write a compose file or a `.env` line, and every
32
+ * caller in this package means "unset" by it. See `resolveFunctionsTimeoutMs`,
33
+ * which learned that the hard way.
34
+ */
35
+ export declare function hostEnvVar(name: string): string | undefined;
36
+ /**
37
+ * Publish an environment bag for contextless readers.
38
+ *
39
+ * Called by an adapter for a runtime whose environment is not ambient. Merges
40
+ * rather than replaces, so two adapters (or an adapter plus a test) do not
41
+ * silently erase each other's variables.
42
+ */
43
+ export declare function setHostEnv(env: Record<string, string | undefined>): void;
44
+ /** @internal Test seam — drops anything {@link setHostEnv} published. */
45
+ export declare function _clearHostEnv(): void;
46
+ /**
47
+ * Write one already-formatted line to the process's output.
48
+ *
49
+ * `process.stdout.write` is preferred where it exists because it is the only
50
+ * one of the two that does not append its own formatting to a line that is
51
+ * already a complete JSON document — `console.log` on Node is
52
+ * `process.stdout.write` plus `util.format`, and `util.format` will happily
53
+ * reinterpret a `%s` that appeared inside a user's log message.
54
+ *
55
+ * Where it does not exist, `console` is the runtime's log sink and is what its
56
+ * platform collects.
57
+ */
58
+ export declare function writeLine(stream: "out" | "err", line: string): void;
@@ -1,18 +1,3 @@
1
- /**
2
- * Structured Logger for Rebase Backend
3
- *
4
- * Outputs JSON lines when `NODE_ENV=production`, human-readable prefixed
5
- * lines otherwise. Designed to work with Google Cloud Logging severity levels.
6
- *
7
- * Every line — message and data, at any depth — passes through the redaction
8
- * below, which strips Drizzle's `Failed query: … / params: …` wrapper and the
9
- * values of secret-looking keys. See the block above `serialiseError`.
10
- *
11
- * Usage:
12
- * import { logger } from "./utils/logger";
13
- * logger.info("Server started", { port: 3001 });
14
- * logger.error("Request failed", { path: "/api/test", error: err });
15
- */
16
1
  export type LogLevel = "debug" | "info" | "warn" | "error";
17
2
  export interface LogEntry {
18
3
  severity: string;