@rebasepro/server 0.13.0 → 0.13.1-canary.g06dbe5b

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 (125) hide show
  1. package/dist/{admin_block-CcSGdH7M.js → admin_block-H86dCPsj.js} +62 -2
  2. package/dist/admin_block-H86dCPsj.js.map +1 -0
  3. package/dist/api/ast-schema-editor.d.ts +74 -6
  4. package/dist/api/errors.d.ts +0 -6
  5. package/dist/api/openapi-generator.d.ts +14 -0
  6. package/dist/api/rest/api-generator.d.ts +1 -1
  7. package/dist/api/rest/idempotency.d.ts +62 -4
  8. package/dist/api/rest/query-parser.d.ts +15 -1
  9. package/dist/api/rest/write-validation.d.ts +52 -9
  10. package/dist/auth/adapter-middleware.d.ts +3 -1
  11. package/dist/auth/apple-oauth.d.ts +16 -11
  12. package/dist/auth/bitbucket-oauth.d.ts +2 -4
  13. package/dist/auth/builtin-auth-adapter.d.ts +2 -0
  14. package/dist/auth/discord-oauth.d.ts +5 -6
  15. package/dist/auth/facebook-oauth.d.ts +2 -4
  16. package/dist/auth/github-oauth.d.ts +2 -4
  17. package/dist/auth/gitlab-oauth.d.ts +6 -4
  18. package/dist/auth/google-oauth.d.ts +1 -0
  19. package/dist/auth/index.d.ts +8 -1
  20. package/dist/auth/interfaces.d.ts +68 -1
  21. package/dist/auth/jwt.d.ts +40 -0
  22. package/dist/auth/linkedin-oauth.d.ts +2 -4
  23. package/dist/auth/mfa-gate.d.ts +42 -0
  24. package/dist/auth/mfa-routes.d.ts +26 -1
  25. package/dist/auth/mfa.d.ts +16 -0
  26. package/dist/auth/microsoft-oauth.d.ts +19 -4
  27. package/dist/auth/oauth-code-flow.d.ts +66 -0
  28. package/dist/auth/oauth-signin-policy.d.ts +61 -0
  29. package/dist/auth/oidc-id-token.d.ts +61 -0
  30. package/dist/auth/rate-limiter.d.ts +37 -2
  31. package/dist/auth/rls-scope.d.ts +25 -0
  32. package/dist/auth/routes.d.ts +8 -0
  33. package/dist/auth/slack-oauth.d.ts +2 -4
  34. package/dist/auth/spotify-oauth.d.ts +2 -4
  35. package/dist/auth/twitter-oauth.d.ts +2 -5
  36. package/dist/{auth-CuC9M2x6.js → auth-DkAHqvf5.js} +1604 -391
  37. package/dist/auth-DkAHqvf5.js.map +1 -0
  38. package/dist/{backup-CVggVhR2.js → backup-DLluVyuA.js} +2 -2
  39. package/dist/{backup-CVggVhR2.js.map → backup-DLluVyuA.js.map} +1 -1
  40. package/dist/boot/boot.d.ts +19 -0
  41. package/dist/boot/ddl-bootstrap.d.ts +70 -0
  42. package/dist/{contract-routes-Dj8i5AiM.js → contract-routes-BB1U05sS.js} +3 -3
  43. package/dist/{contract-routes-Dj8i5AiM.js.map → contract-routes-BB1U05sS.js.map} +1 -1
  44. package/dist/cron/cron-scheduler.d.ts +8 -3
  45. package/dist/cron/define-cron.d.ts +17 -3
  46. package/dist/{cron-loader-B1S2MCSl.js → cron-loader-3U5aGILy.js} +2 -2
  47. package/dist/{cron-loader-B1S2MCSl.js.map → cron-loader-3U5aGILy.js.map} +1 -1
  48. package/dist/{cron-routes-CrQ0tK-_.js → cron-routes-rZgwlOz4.js} +2 -2
  49. package/dist/{cron-routes-CrQ0tK-_.js.map → cron-routes-rZgwlOz4.js.map} +1 -1
  50. package/dist/{cron-scheduler-B3RFt0HS.js → cron-scheduler-FJAaCAXm.js} +30 -5
  51. package/dist/cron-scheduler-FJAaCAXm.js.map +1 -0
  52. package/dist/{cron-store-BywZsyfZ.js → cron-store-9NmUDfzL.js} +66 -47
  53. package/dist/cron-store-9NmUDfzL.js.map +1 -0
  54. package/dist/ddl-bootstrap-BhXbTnBl.js +183 -0
  55. package/dist/ddl-bootstrap-BhXbTnBl.js.map +1 -0
  56. package/dist/email/html.d.ts +54 -0
  57. package/dist/email/index.d.ts +3 -0
  58. package/dist/email/link-base.d.ts +39 -0
  59. package/dist/email/smtp-email-service.d.ts +7 -1
  60. package/dist/email/templates.d.ts +9 -1
  61. package/dist/email/types.d.ts +11 -1
  62. package/dist/{errors-CgkCzoj7.js → errors-B1WZEdsK.js} +3 -11
  63. package/dist/errors-B1WZEdsK.js.map +1 -0
  64. package/dist/function-loader-D1SwtCa5.js +139 -0
  65. package/dist/function-loader-D1SwtCa5.js.map +1 -0
  66. package/dist/{function-routes-C0cLIy3N.js → function-routes-Btcez1T-.js} +18 -6
  67. package/dist/function-routes-Btcez1T-.js.map +1 -0
  68. package/dist/functions/define-function.d.ts +25 -6
  69. package/dist/functions/function-loader.d.ts +23 -0
  70. package/dist/functions/function-routes.d.ts +7 -1
  71. package/dist/functions/request-timeout.d.ts +34 -0
  72. package/dist/history/history-routes.d.ts +6 -0
  73. package/dist/index.d.ts +2 -1
  74. package/dist/index.es.js +1960 -794
  75. package/dist/index.es.js.map +1 -1
  76. package/dist/init/docs.d.ts +10 -1
  77. package/dist/init/process-safety.d.ts +25 -0
  78. package/dist/init.d.ts +27 -0
  79. package/dist/{jwt-DD6EtpGj.js → jwt-Cuq6MwNy.js} +70 -8
  80. package/dist/jwt-Cuq6MwNy.js.map +1 -0
  81. package/dist/logger-DfvF_8r-.js +190 -0
  82. package/dist/logger-DfvF_8r-.js.map +1 -0
  83. package/dist/openapi-generator-BQxxxDpb.js +865 -0
  84. package/dist/openapi-generator-BQxxxDpb.js.map +1 -0
  85. package/dist/request-timeout-RivJsME0.js +65 -0
  86. package/dist/request-timeout-RivJsME0.js.map +1 -0
  87. package/dist/schema-editor-routes-DZFKGXgq.js +437 -0
  88. package/dist/schema-editor-routes-DZFKGXgq.js.map +1 -0
  89. package/dist/serve-spa.d.ts +5 -4
  90. package/dist/services/outbound-url-guard.d.ts +53 -0
  91. package/dist/services/routed-realtime-service.d.ts +10 -2
  92. package/dist/services/webhook-service.d.ts +76 -1
  93. package/dist/singleton.d.ts +25 -8
  94. package/dist/{src-Cum9kox5.js → src-By48Ffg0.js} +322 -70
  95. package/dist/src-By48Ffg0.js.map +1 -0
  96. package/dist/{src-_qQ3RNCK.js → src-Ca6NhxKs.js} +79 -2
  97. package/dist/src-Ca6NhxKs.js.map +1 -0
  98. package/dist/storage/LocalStorageController.d.ts +12 -1
  99. package/dist/storage/image-transform.d.ts +57 -0
  100. package/dist/storage/keys.d.ts +98 -0
  101. package/dist/storage/routes.d.ts +11 -0
  102. package/dist/storage/tus-handler.d.ts +7 -1
  103. package/dist/utils/logger.d.ts +18 -0
  104. package/dist/utils/logging.d.ts +0 -4
  105. package/dist/utils/sql.d.ts +11 -6
  106. package/package.json +7 -6
  107. package/dist/admin_block-CcSGdH7M.js.map +0 -1
  108. package/dist/auth-CuC9M2x6.js.map +0 -1
  109. package/dist/backend-CIxN4FVm.js +0 -15
  110. package/dist/backend-CIxN4FVm.js.map +0 -1
  111. package/dist/cron-scheduler-B3RFt0HS.js.map +0 -1
  112. package/dist/cron-store-BywZsyfZ.js.map +0 -1
  113. package/dist/errors-CgkCzoj7.js.map +0 -1
  114. package/dist/function-loader-B_1fYfUY.js +0 -86
  115. package/dist/function-loader-B_1fYfUY.js.map +0 -1
  116. package/dist/function-routes-C0cLIy3N.js.map +0 -1
  117. package/dist/jwt-DD6EtpGj.js.map +0 -1
  118. package/dist/logger-BYU66ENZ.js +0 -94
  119. package/dist/logger-BYU66ENZ.js.map +0 -1
  120. package/dist/openapi-generator-BEwyiaAr.js +0 -597
  121. package/dist/openapi-generator-BEwyiaAr.js.map +0 -1
  122. package/dist/schema-editor-routes-CbF20Mf1.js +0 -248
  123. package/dist/schema-editor-routes-CbF20Mf1.js.map +0 -1
  124. package/dist/src-Cum9kox5.js.map +0 -1
  125. package/dist/src-_qQ3RNCK.js.map +0 -1
@@ -0,0 +1,437 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ import "process";
3
+ __createRequire(import.meta.url);
4
+ import "./src-Ca6NhxKs.js";
5
+ import { i as nestAdminPropertyKeys, r as nestAdminCollectionKeys } from "./admin_block-H86dCPsj.js";
6
+ import { n as errorHandler } from "./errors-B1WZEdsK.js";
7
+ import * as fs$1 from "fs";
8
+ import * as path$1 from "path";
9
+ import { Hono } from "hono";
10
+ import { IndentationText, Node, Project, SyntaxKind } from "ts-morph";
11
+ //#region src/api/ast-schema-editor.ts
12
+ /**
13
+ * The helpers a collection file may be wrapped in.
14
+ *
15
+ * `rebase init` scaffolds every collection as
16
+ * `const postsCollection = defineCollection({ … })` — a call expression, not the
17
+ * bare object literal `rebase introspect` emits. An editor that only understood
18
+ * the bare form found nothing to patch in any stock project, and then rewrote the
19
+ * file from the panel's JSON: no wrapper, no imports, no relation thunks.
20
+ */
21
+ var COLLECTION_FACTORIES = /* @__PURE__ */ new Set(["defineCollection"]);
22
+ /**
23
+ * A value that must be emitted as source code rather than as JSON.
24
+ *
25
+ * Everything reaching the writer has been through `JSON.stringify` on the wire,
26
+ * so a function-valued key arrives either missing or as a string. A relation's
27
+ * `target` is a thunk in the file and a slug in the payload; this is how the
28
+ * thunk gets written back.
29
+ */
30
+ var RawExpression = class {
31
+ text;
32
+ constructor(text) {
33
+ this.text = text;
34
+ }
35
+ };
36
+ /**
37
+ * Move presentation keys into the `admin` block.
38
+ *
39
+ * The rule itself lives in `@rebasepro/types`, next to `ADMIN_COLLECTION_KEYS`,
40
+ * because `@rebasepro/admin-types` has to apply the identical one on the panel's
41
+ * side and this package cannot import that one. Two copies used to exist and
42
+ * they disagreed about precedence, which decided whether a presentation edit was
43
+ * saved or silently reverted to the value the user had just changed away from.
44
+ */
45
+ function nestAdminKeys(collectionData) {
46
+ return nestAdminCollectionKeys(collectionData);
47
+ }
48
+ var AstSchemaEditor = class AstSchemaEditor {
49
+ project;
50
+ collectionsDir;
51
+ constructor(collectionsDir) {
52
+ this.project = new Project({ manipulationSettings: { indentationText: IndentationText.FourSpaces } });
53
+ if (fs$1.existsSync(collectionsDir)) this.project.addSourceFilesAtPaths(`${collectionsDir}/**/*.ts`);
54
+ this.collectionsDir = path$1.resolve(collectionsDir);
55
+ }
56
+ /**
57
+ * Sanitize collectionId to prevent path traversal attacks.
58
+ * Only allows alphanumeric characters, underscores, and hyphens.
59
+ */
60
+ sanitizeCollectionId(collectionId) {
61
+ const sanitized = collectionId.replace(/[^a-zA-Z0-9_-]/g, "");
62
+ if (!sanitized || sanitized !== collectionId) throw new Error(`Invalid collection ID: "${collectionId}". Only alphanumeric characters, underscores, and hyphens are allowed.`);
63
+ return sanitized;
64
+ }
65
+ /**
66
+ * Resolve a file path and ensure it falls within the collectionsDir.
67
+ */
68
+ safePath(filename) {
69
+ const resolved = path$1.resolve(this.collectionsDir, filename);
70
+ if (!resolved.startsWith(this.collectionsDir + path$1.sep) && resolved !== this.collectionsDir) throw new Error("Path traversal detected: resolved path is outside the collections directory.");
71
+ return resolved;
72
+ }
73
+ getCollectionFile(collectionId) {
74
+ const safeId = this.sanitizeCollectionId(collectionId);
75
+ const filePath = this.safePath(`${safeId}.ts`);
76
+ let file = this.project.getSourceFile(filePath);
77
+ if (!file && fs$1.existsSync(filePath)) {
78
+ this.project.addSourceFilesAtPaths(`${this.collectionsDir}/**/*.ts`);
79
+ file = this.project.getSourceFile(filePath);
80
+ }
81
+ return file;
82
+ }
83
+ /**
84
+ * Find the object literal a collection is declared with, through whatever
85
+ * wraps it.
86
+ *
87
+ * `defineCollection({ … })` is the shape `rebase init` writes and the one the
88
+ * docs recommend; `satisfies` / `as` / parentheses are the other ways an
89
+ * author can dress the same literal. Returning `null` for any of them meant
90
+ * the three callers below each failed differently, and the worst of them
91
+ * overwrote the file.
92
+ */
93
+ unwrapCollectionObject(node) {
94
+ if (!node) return null;
95
+ if (Node.isObjectLiteralExpression(node)) return node;
96
+ if (Node.isParenthesizedExpression(node) || Node.isAsExpression(node) || Node.isSatisfiesExpression(node) || Node.isTypeAssertion(node) || Node.isNonNullExpression(node)) return this.unwrapCollectionObject(node.getExpression());
97
+ if (Node.isCallExpression(node)) {
98
+ const callee = node.getExpression().getText().split(".").pop();
99
+ if (callee && COLLECTION_FACTORIES.has(callee)) return this.unwrapCollectionObject(node.getArguments()[0]);
100
+ }
101
+ return null;
102
+ }
103
+ getCollectionObject(collectionId) {
104
+ const file = this.getCollectionFile(collectionId);
105
+ if (!file) return null;
106
+ const defaultExport = file.getDefaultExportSymbol();
107
+ if (defaultExport) {
108
+ const declaration = defaultExport.getDeclarations()[0];
109
+ if (declaration && declaration.getKind() === SyntaxKind.ExportAssignment) {
110
+ const expr = declaration.asKind(SyntaxKind.ExportAssignment)?.getExpression();
111
+ if (expr && expr.getKind() === SyntaxKind.Identifier) {
112
+ const varName = expr.getText();
113
+ const varDecl = file.getVariableDeclaration(varName);
114
+ const unwrapped = this.unwrapCollectionObject(varDecl?.getInitializer());
115
+ if (unwrapped) return unwrapped;
116
+ } else {
117
+ const unwrapped = this.unwrapCollectionObject(expr);
118
+ if (unwrapped) return unwrapped;
119
+ }
120
+ }
121
+ }
122
+ for (const varDecl of file.getVariableDeclarations()) {
123
+ const init = this.unwrapCollectionObject(varDecl.getInitializer());
124
+ if (init) return init;
125
+ }
126
+ return null;
127
+ }
128
+ /**
129
+ * The collection's object literal, or a refusal that says what to do.
130
+ *
131
+ * Every caller needs this to be all-or-nothing: a missing object literal used
132
+ * to mean "throw", "report success and do nothing" and "recreate the file
133
+ * from scratch" depending on which method you called.
134
+ */
135
+ requireCollectionObject(collectionId) {
136
+ const file = this.getCollectionFile(collectionId);
137
+ if (!file) throw new Error(`Collection "${collectionId}" has no file at ${path$1.join(this.collectionsDir, `${collectionId}.ts`)}.`);
138
+ const collectionObj = this.getCollectionObject(collectionId);
139
+ if (!collectionObj) throw new Error(this.unreadableFileMessage(collectionId, file));
140
+ return collectionObj;
141
+ }
142
+ unreadableFileMessage(collectionId, file) {
143
+ return `Could not find the collection object in ${file.getFilePath()}. The schema editor can only edit a collection declared as \`const x = defineCollection({ … })\` or \`const x: CollectionConfig = { … }\` and exported as the file's default. Edit "${collectionId}" in code instead.`;
144
+ }
145
+ /** Look a key up on an object literal, quoted or not. */
146
+ findProperty(obj, name) {
147
+ return obj.getProperty((p) => "getName" in p && typeof p.getName === "function" && (p.getName() === name || p.getName() === `"${name}"` || p.getName() === `'${name}'`));
148
+ }
149
+ static quoteKey(key) {
150
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
151
+ }
152
+ static isPlainObject(value) {
153
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof RawExpression);
154
+ }
155
+ convertJsonToAstString(obj, indentLevel = 0, oldAstNode) {
156
+ const indentStr = " ";
157
+ const indent = indentStr.repeat(indentLevel);
158
+ const innerIndent = indentStr.repeat(indentLevel + 1);
159
+ if (obj instanceof RawExpression) return obj.text;
160
+ if (obj === null || obj === void 0) return "undefined";
161
+ if (typeof obj === "string") return JSON.stringify(obj);
162
+ if (typeof obj === "number" || typeof obj === "boolean") return String(obj);
163
+ if (Array.isArray(obj)) {
164
+ if (obj.length === 0) return "[]";
165
+ return `[\n${innerIndent}${obj.map((item) => this.convertJsonToAstString(item, indentLevel + 1)).join(`,\n${innerIndent}`)}\n${indent}]`;
166
+ }
167
+ if (typeof obj === "object") {
168
+ const record = obj;
169
+ const keys = Object.keys(record);
170
+ const preservedProps = [];
171
+ if (oldAstNode) {
172
+ const oldProps = oldAstNode.getProperties();
173
+ for (const oldProp of oldProps) if (oldProp.isKind(SyntaxKind.PropertyAssignment)) {
174
+ let name = oldProp.getNameNode().getText();
175
+ if (name.startsWith("\"") && name.endsWith("\"")) name = name.slice(1, -1);
176
+ if (name.startsWith("'") && name.endsWith("'")) name = name.slice(1, -1);
177
+ if (!(name in record)) {
178
+ const init = oldProp.getInitializer();
179
+ if (init) {
180
+ const kind = init.getKind();
181
+ if (kind === SyntaxKind.ArrowFunction || kind === SyntaxKind.FunctionExpression || kind === SyntaxKind.Identifier || kind === SyntaxKind.CallExpression || kind === SyntaxKind.JsxElement || name === "target" || name === "callbacks" || name === "permissions" || name === "securityRules") preservedProps.push(`${AstSchemaEditor.quoteKey(name)}: ${init.getText()}`);
182
+ }
183
+ }
184
+ }
185
+ }
186
+ if (keys.length === 0 && preservedProps.length === 0) return "{}";
187
+ return `{\n${innerIndent}${[...keys.map((key) => {
188
+ const keyStr = AstSchemaEditor.quoteKey(key);
189
+ let childAstNode;
190
+ if (oldAstNode && AstSchemaEditor.isPlainObject(record[key])) {
191
+ const oldProp = this.findProperty(oldAstNode, key);
192
+ if (oldProp && oldProp.isKind(SyntaxKind.PropertyAssignment)) childAstNode = oldProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);
193
+ }
194
+ return `${keyStr}: ${this.convertJsonToAstString(record[key], indentLevel + 1, childAstNode)}`;
195
+ }), ...preservedProps].join(`,\n${innerIndent}`)}\n${indent}}`;
196
+ }
197
+ return "undefined";
198
+ }
199
+ /**
200
+ * Write only the keys the patch names, leaving every sibling alone.
201
+ *
202
+ * A patch says what changed, not what the collection is. The panel sends one
203
+ * — `{ propertiesOrder }` is what adding a column posts — and rewriting the
204
+ * `admin` block from it deleted the collection's icon, group, list columns
205
+ * and kanban config in the same write.
206
+ */
207
+ mergeIntoObjectLiteral(target, data, indentLevel) {
208
+ for (const [key, value] of Object.entries(data)) {
209
+ const existing = this.findProperty(target, key);
210
+ const existingObj = existing && existing.isKind(SyntaxKind.PropertyAssignment) ? existing.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression) : void 0;
211
+ if (existingObj && AstSchemaEditor.isPlainObject(value)) {
212
+ this.mergeIntoObjectLiteral(existingObj, value, indentLevel + 1);
213
+ continue;
214
+ }
215
+ const initializer = this.convertJsonToAstString(value, indentLevel, existingObj);
216
+ if (existing && existing.isKind(SyntaxKind.PropertyAssignment)) existing.setInitializer(initializer);
217
+ else target.addPropertyAssignment({
218
+ name: AstSchemaEditor.quoteKey(key),
219
+ initializer
220
+ });
221
+ }
222
+ }
223
+ async saveProperty(collectionId, propertyKey, propertyConfig) {
224
+ const collectionObj = this.requireCollectionObject(collectionId);
225
+ const nestedConfig = nestAdminPropertyKeys(propertyConfig);
226
+ let propertiesProp = collectionObj.getProperty("properties");
227
+ if (!propertiesProp) propertiesProp = collectionObj.addPropertyAssignment({
228
+ name: "properties",
229
+ initializer: "{}"
230
+ });
231
+ const propsObj = propertiesProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);
232
+ if (propsObj) {
233
+ const existingProp = this.findProperty(propsObj, propertyKey);
234
+ let oldPropAstNode;
235
+ if (existingProp && existingProp.isKind(SyntaxKind.PropertyAssignment)) oldPropAstNode = existingProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);
236
+ const newInitializer = this.convertJsonToAstString(nestedConfig, 2, oldPropAstNode);
237
+ if (existingProp) {
238
+ if (existingProp.isKind(SyntaxKind.PropertyAssignment)) existingProp.setInitializer(newInitializer);
239
+ } else propsObj.addPropertyAssignment({
240
+ name: AstSchemaEditor.quoteKey(propertyKey),
241
+ initializer: newInitializer
242
+ });
243
+ const file = this.getCollectionFile(collectionId);
244
+ if (file) file.formatText();
245
+ await this.project.save();
246
+ }
247
+ }
248
+ async deleteProperty(collectionId, propertyKey) {
249
+ const propertiesProp = this.requireCollectionObject(collectionId).getProperty("properties");
250
+ if (propertiesProp) {
251
+ const propsObj = propertiesProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);
252
+ if (propsObj) {
253
+ const existingProp = this.findProperty(propsObj, propertyKey);
254
+ if (existingProp) {
255
+ existingProp.remove();
256
+ const file = this.getCollectionFile(collectionId);
257
+ if (file) file.formatText();
258
+ await this.project.save();
259
+ }
260
+ }
261
+ }
262
+ }
263
+ /**
264
+ * Write a collection back to its file.
265
+ *
266
+ * `partial` is the difference between "this is the collection" and "this is
267
+ * what changed about it". The panel sends both — a full save from the editor
268
+ * dialog, and a one-key patch whenever a property is added, deleted or
269
+ * reordered — and they cannot be told apart by looking at the payload. Read
270
+ * as a full save, a patch deletes everything it does not mention, including
271
+ * `securityRules`; the loader then hands the collection the directory
272
+ * default, which in the scaffold is `access: "public"`.
273
+ */
274
+ async saveCollection(collectionId, collectionData, options = {}) {
275
+ const partial = options.partial === true;
276
+ let file = this.getCollectionFile(collectionId);
277
+ const collectionObj = file ? this.getCollectionObject(collectionId) : null;
278
+ if (file && !collectionObj) throw new Error(this.unreadableFileMessage(collectionId, file));
279
+ if (!file || !collectionObj) {
280
+ if (partial) throw new Error(`Cannot apply a partial update to "${collectionId}": it has no collection file yet.`);
281
+ const safeId = this.sanitizeCollectionId(collectionId);
282
+ const newFilePath = this.safePath(`${safeId}.ts`);
283
+ if (fs$1.existsSync(newFilePath)) throw new Error(`Refusing to overwrite ${newFilePath}: a file for "${collectionId}" already exists but could not be parsed.`);
284
+ 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`);
285
+ } else {
286
+ if (!partial) {
287
+ if (!("securityRules" in collectionData) || collectionData.securityRules === void 0 || Array.isArray(collectionData.securityRules) && collectionData.securityRules.length === 0) {
288
+ const srProp = collectionObj.getProperty("securityRules");
289
+ if (srProp) srProp.remove();
290
+ delete collectionData["securityRules"];
291
+ }
292
+ }
293
+ collectionData = nestAdminKeys(collectionData);
294
+ for (const key of Object.keys(collectionData)) {
295
+ if (key === "relations") {
296
+ this.writeRelations(collectionId, file, collectionObj, collectionData[key]);
297
+ continue;
298
+ }
299
+ const prop = collectionObj.getProperty(key);
300
+ let oldAstNode;
301
+ if (prop && prop.isKind(SyntaxKind.PropertyAssignment)) oldAstNode = prop.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);
302
+ if (partial && oldAstNode && AstSchemaEditor.isPlainObject(collectionData[key])) {
303
+ this.mergeIntoObjectLiteral(oldAstNode, collectionData[key], 2);
304
+ continue;
305
+ }
306
+ const newInit = this.convertJsonToAstString(collectionData[key], 1, oldAstNode);
307
+ if (prop) prop.setInitializer(newInit);
308
+ else collectionObj.addPropertyAssignment({
309
+ name: key,
310
+ initializer: newInit
311
+ });
312
+ }
313
+ }
314
+ if (file) file.formatText();
315
+ await this.project.save();
316
+ }
317
+ /**
318
+ * Write the collection-level `relations` array.
319
+ *
320
+ * Relations are the one key whose values are not all data: `target` is a
321
+ * thunk in the file, and the panel sends the target collection's slug (or,
322
+ * for a relation it did not touch, nothing at all — `JSON.stringify` drops
323
+ * the function). So each entry's target is resolved in that order: a slug
324
+ * becomes `() => xCollection` plus the import it needs, and anything else
325
+ * falls back to the thunk already in the file.
326
+ *
327
+ * This used to be a `continue` with a comment saying relations were handled
328
+ * elsewhere. Nothing handled them; every edit in the Relations tab was
329
+ * dropped without a word.
330
+ */
331
+ writeRelations(collectionId, file, collectionObj, value) {
332
+ if (!Array.isArray(value)) return;
333
+ const existing = this.findProperty(collectionObj, "relations");
334
+ const oldElements = (existing && existing.isKind(SyntaxKind.PropertyAssignment) ? existing.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression) : void 0)?.getElements() ?? [];
335
+ const oldTargetsByName = /* @__PURE__ */ new Map();
336
+ const anonymousTargetsByIndex = [];
337
+ oldElements.forEach((element, index) => {
338
+ const elementObj = element.asKind(SyntaxKind.ObjectLiteralExpression);
339
+ if (!elementObj) return;
340
+ const targetProp = this.findProperty(elementObj, "target");
341
+ const targetInit = targetProp && targetProp.isKind(SyntaxKind.PropertyAssignment) ? targetProp.getInitializer() : void 0;
342
+ if (!targetInit) return;
343
+ const nameProp = this.findProperty(elementObj, "relationName");
344
+ const nameInit = nameProp && nameProp.isKind(SyntaxKind.PropertyAssignment) ? nameProp.getInitializerIfKind(SyntaxKind.StringLiteral) : void 0;
345
+ if (nameInit) oldTargetsByName.set(nameInit.getLiteralValue(), targetInit.getText());
346
+ else anonymousTargetsByIndex[index] = targetInit.getText();
347
+ });
348
+ const items = value.map((entry, index) => {
349
+ if (!AstSchemaEditor.isPlainObject(entry)) return entry;
350
+ const relation = { ...entry };
351
+ const relationName = typeof relation.relationName === "string" ? relation.relationName : void 0;
352
+ const rawTarget = relation.target;
353
+ let targetText;
354
+ if (typeof rawTarget === "string" && rawTarget.trim().length > 0) targetText = rawTarget.includes("=>") ? rawTarget.trim() : this.targetThunk(file, rawTarget.trim());
355
+ else if (relationName && oldTargetsByName.has(relationName)) targetText = oldTargetsByName.get(relationName);
356
+ else if (value.length === oldElements.length) targetText = anonymousTargetsByIndex[index];
357
+ if (!targetText) throw new Error(`Relation "${relationName ?? `#${index}`}" on collection "${collectionId}" has no target collection. Pick one before saving.`);
358
+ relation.target = new RawExpression(targetText);
359
+ return relation;
360
+ });
361
+ const initializer = this.convertJsonToAstString(items, 1);
362
+ if (existing && existing.isKind(SyntaxKind.PropertyAssignment)) existing.setInitializer(initializer);
363
+ else collectionObj.addPropertyAssignment({
364
+ name: "relations",
365
+ initializer
366
+ });
367
+ }
368
+ /**
369
+ * `() => targetCollection` for a target named by its slug, importing it if
370
+ * the file does not already.
371
+ */
372
+ targetThunk(file, targetSlug) {
373
+ const targetFile = this.getCollectionFile(targetSlug);
374
+ if (!targetFile) throw new Error(`Cannot link to collection "${targetSlug}": no file for it in ${this.collectionsDir}.`);
375
+ const identifier = this.getDefaultExportName(targetFile);
376
+ if (!identifier) throw new Error(`Cannot link to collection "${targetSlug}": ${targetFile.getFilePath()} has no default export to import.`);
377
+ if (targetFile.getFilePath() !== file.getFilePath() && !this.hasDefaultImport(file, identifier)) {
378
+ const relative = path$1.relative(path$1.dirname(file.getFilePath()), targetFile.getFilePath()).split(path$1.sep).join("/").replace(/\.tsx?$/, ".js");
379
+ file.addImportDeclaration({
380
+ defaultImport: identifier,
381
+ moduleSpecifier: relative.startsWith(".") ? relative : `./${relative}`
382
+ });
383
+ }
384
+ return `() => ${identifier}`;
385
+ }
386
+ hasDefaultImport(file, identifier) {
387
+ return file.getImportDeclarations().some((decl) => decl.getDefaultImport()?.getText() === identifier);
388
+ }
389
+ getDefaultExportName(file) {
390
+ const expr = (file.getDefaultExportSymbol()?.getDeclarations()[0])?.asKind(SyntaxKind.ExportAssignment)?.getExpression();
391
+ if (expr && expr.getKind() === SyntaxKind.Identifier) return expr.getText();
392
+ for (const varDecl of file.getVariableDeclarations()) if (this.unwrapCollectionObject(varDecl.getInitializer())) return varDecl.getName();
393
+ }
394
+ async deleteCollection(collectionId) {
395
+ const file = this.getCollectionFile(collectionId);
396
+ if (file) file.deleteImmediatelySync();
397
+ }
398
+ };
399
+ //#endregion
400
+ //#region src/api/schema-editor-routes.ts
401
+ function createSchemaEditorRoutes(collectionsDir) {
402
+ const router = new Hono();
403
+ router.onError(errorHandler);
404
+ const editor = new AstSchemaEditor(collectionsDir);
405
+ router.post("/property/save", async (c) => {
406
+ const { collectionId, propertyKey, propertyConfig } = await c.req.json();
407
+ await editor.saveProperty(collectionId, propertyKey, propertyConfig);
408
+ return c.json({ success: true });
409
+ });
410
+ router.post("/property/delete", async (c) => {
411
+ const { collectionId, propertyKey } = await c.req.json();
412
+ await editor.deleteProperty(collectionId, propertyKey);
413
+ return c.json({ success: true });
414
+ });
415
+ /**
416
+ * `partial: true` means "this payload is what changed", not "this is the
417
+ * collection". Without it a one-key patch — which is what adding a column
418
+ * posts — is read as a whole-collection save and deletes everything it does
419
+ * not mention, `securityRules` included. Absent, it defaults to a full save,
420
+ * so an older panel keeps the behaviour it was written against.
421
+ */
422
+ router.post("/collection/save", async (c) => {
423
+ const { collectionId, collectionData, partial } = await c.req.json();
424
+ await editor.saveCollection(collectionId, collectionData, { partial: partial === true });
425
+ return c.json({ success: true });
426
+ });
427
+ router.post("/collection/delete", async (c) => {
428
+ const { collectionId } = await c.req.json();
429
+ await editor.deleteCollection(collectionId);
430
+ return c.json({ success: true });
431
+ });
432
+ return router;
433
+ }
434
+ //#endregion
435
+ export { createSchemaEditorRoutes };
436
+
437
+ //# sourceMappingURL=schema-editor-routes-DZFKGXgq.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-editor-routes-DZFKGXgq.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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"}
@@ -58,9 +58,10 @@ export interface ServeSPAConfig {
58
58
  * Serve a Single Page Application from an Hono app.
59
59
  *
60
60
  * @internal Not part of the stable public API. Exported only because the
61
- * official app template (`packages/cli/templates/template/backend/src/index.ts`
62
- * and `app/backend/src/index.ts`) calls it to serve the built frontend in
63
- * production. Its request-handling behavior is an implementation detail and
64
- * may change without a major version bump.
61
+ * entrypoint `rebase eject` writes
62
+ * (`packages/cli/templates/eject/backend/src/index.ts`) and `app/backend/src/index.ts`
63
+ * call it to serve the built frontend in production — the scaffolded template
64
+ * has no entrypoint of its own. Its request-handling behavior is an
65
+ * implementation detail and may change without a major version bump.
65
66
  */
66
67
  export declare function serveSPA<E extends import("hono").Env>(app: Hono<E>, config: ServeSPAConfig): void;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Refusal to send a request to a destination the guard does not allow.
3
+ *
4
+ * Distinct from a network error on purpose: a blocked destination is
5
+ * *terminal* — retrying it re-runs the same refusal — while an `ENOTFOUND`
6
+ * or a refused connection is worth another attempt.
7
+ */
8
+ export declare class BlockedUrlError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ export interface OutboundUrlGuardOptions {
12
+ /**
13
+ * Send to destinations that resolve to loopback, link-local, private or
14
+ * otherwise non-public addresses. Off by default, and turning it on
15
+ * re-opens SSRF: any caller who can choose the URL can then reach the
16
+ * pod's own metadata endpoint, the cluster API server and the database.
17
+ * Only for a genuinely local receiver (a sidecar, a dev tunnel).
18
+ */
19
+ allowPrivateNetworks?: boolean;
20
+ /**
21
+ * Hostname resolver. Defaults to `dns.lookup(host, { all: true })`.
22
+ * Injected by tests so a unit suite never touches a resolver.
23
+ */
24
+ lookup?: (hostname: string) => Promise<string[]>;
25
+ }
26
+ /**
27
+ * Why this address must not be a webhook destination, or `null` if it is an
28
+ * ordinary public address. Exported for tests and for anyone else adding an
29
+ * outbound call whose destination comes from data.
30
+ */
31
+ export declare function blockedAddressReason(address: string): string | null;
32
+ /**
33
+ * Resolve and validate a destination before anything connects to it.
34
+ *
35
+ * Three checks, in order, because each is cheaper than the next: the scheme,
36
+ * the hostname, then every address the hostname resolves to. *Every* address —
37
+ * a name that answers with one public and one private record is a rebinding
38
+ * attempt, and taking the first record would let it through.
39
+ *
40
+ * Known limit: this is validate-then-fetch. Between the lookup here and the
41
+ * connection the runtime makes there is a second resolution this code does not
42
+ * see, so a TTL-0 name that flips between answers can still land on a blocked
43
+ * address. Closing that needs the socket pinned to the address checked here,
44
+ * which Node's global `fetch` does not expose without an undici dispatcher.
45
+ * The redirect policy in {@link WebhookDispatcher} closes the much wider
46
+ * version of the same hole — a receiver that simply *points* at the internal
47
+ * address.
48
+ *
49
+ * @throws BlockedUrlError when the destination is not allowed. DNS failures
50
+ * are rethrown as they are, so callers can tell "never send this" from
51
+ * "could not send this right now".
52
+ */
53
+ export declare function assertAllowedOutboundUrl(rawUrl: string, options?: OutboundUrlGuardOptions): Promise<URL>;
@@ -15,6 +15,13 @@ interface ClientMessage {
15
15
  export interface WsRealtimeService extends RealtimeProvider {
16
16
  addClient(clientId: string, ws: unknown): void;
17
17
  handleClientMessage(clientId: string, message: ClientMessage, authContext?: unknown): Promise<void> | void;
18
+ /**
19
+ * Whether this provider actually implements channels, presence and
20
+ * broadcast. Declared by the provider rather than inferred, because the
21
+ * alternative — assuming the default one does — is how these frames came to
22
+ * be handed to an engine whose message switch drops them without a word.
23
+ */
24
+ supportsChannels?: boolean;
18
25
  }
19
26
  export interface RoutedRealtimeOptions {
20
27
  /** Per-engine realtime providers, keyed by data-source key. */
@@ -34,8 +41,9 @@ export interface RoutedRealtimeOptions {
34
41
  * - `subscribe_collection` / `subscribe_entity` → the provider for the
35
42
  * collection's data source (by `payload.path`).
36
43
  * - `unsubscribe` → forwarded to all providers (a no-op on non-owners).
37
- * - channel / presence / broadcast → the default provider (these are global
38
- * pub/sub, not bound to an engine).
44
+ * - channel / presence / broadcast → the first provider that declares
45
+ * `supportsChannels` (these are global pub/sub, not bound to an engine — but
46
+ * they are not implemented by every engine either).
39
47
  * - `addClient` and lifecycle (`onServerReady`/`destroy`/`stopListening`) →
40
48
  * all providers (each registers its own ws close handler for cleanup).
41
49
  */