@happyvertical/smrt-core 0.37.7 → 0.37.9

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 (88) hide show
  1. package/AGENTS.md +25 -6
  2. package/dist/change-feed.d.ts +261 -0
  3. package/dist/change-feed.d.ts.map +1 -0
  4. package/dist/change-feed.js +482 -0
  5. package/dist/change-feed.js.map +1 -0
  6. package/dist/class.d.ts.map +1 -1
  7. package/dist/class.js +4 -2
  8. package/dist/class.js.map +1 -1
  9. package/dist/collection.js +2 -2
  10. package/dist/consumer-plugin/index.d.ts.map +1 -1
  11. package/dist/consumer-plugin/index.js +24 -19
  12. package/dist/consumer-plugin/index.js.map +1 -1
  13. package/dist/dispatch/bus.js +2 -2
  14. package/dist/dispatch/index.d.ts +1 -1
  15. package/dist/dispatch/index.d.ts.map +1 -1
  16. package/dist/dispatch/index.js +2 -2
  17. package/dist/dispatch/tenant-resolver.d.ts +23 -0
  18. package/dist/dispatch/tenant-resolver.d.ts.map +1 -1
  19. package/dist/dispatch/tenant-resolver.js +33 -1
  20. package/dist/dispatch/tenant-resolver.js.map +1 -1
  21. package/dist/generated-client-runtime.d.ts +26 -0
  22. package/dist/generated-client-runtime.d.ts.map +1 -0
  23. package/dist/generated-client-runtime.js +88 -0
  24. package/dist/generated-client-runtime.js.map +1 -0
  25. package/dist/generators/changes-route.d.ts +52 -0
  26. package/dist/generators/changes-route.d.ts.map +1 -0
  27. package/dist/generators/changes-route.js +111 -0
  28. package/dist/generators/changes-route.js.map +1 -0
  29. package/dist/generators/conditional-get.d.ts +12 -0
  30. package/dist/generators/conditional-get.d.ts.map +1 -1
  31. package/dist/generators/conditional-get.js +30 -2
  32. package/dist/generators/conditional-get.js.map +1 -1
  33. package/dist/generators/rest.d.ts +58 -0
  34. package/dist/generators/rest.d.ts.map +1 -1
  35. package/dist/generators/rest.js +114 -21
  36. package/dist/generators/rest.js.map +1 -1
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +6 -5
  40. package/dist/manifest/manifest-loader.d.ts +10 -1
  41. package/dist/manifest/manifest-loader.d.ts.map +1 -1
  42. package/dist/manifest/manifest-loader.js +18 -5
  43. package/dist/manifest/manifest-loader.js.map +1 -1
  44. package/dist/manifest/static-manifest.js +2 -2
  45. package/dist/manifest/static-manifest.js.map +1 -1
  46. package/dist/manifest/store.js +1 -1
  47. package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
  48. package/dist/manifest/test-manifest-stub.js +880 -105
  49. package/dist/manifest/test-manifest-stub.js.map +1 -1
  50. package/dist/manifest.json +2 -2
  51. package/dist/object.js +2 -2
  52. package/dist/prebuild/index.d.ts.map +1 -1
  53. package/dist/prebuild/index.js +14 -7
  54. package/dist/prebuild/index.js.map +1 -1
  55. package/dist/registry/shared-state.d.ts +8 -1
  56. package/dist/registry/shared-state.d.ts.map +1 -1
  57. package/dist/registry/shared-state.js +11 -3
  58. package/dist/registry/shared-state.js.map +1 -1
  59. package/dist/smrt-knowledge.json +6 -6
  60. package/dist/system/schema.d.ts +24 -1
  61. package/dist/system/schema.d.ts.map +1 -1
  62. package/dist/system/schema.js +45 -3
  63. package/dist/system/schema.js.map +1 -1
  64. package/dist/testing/database.js +1 -1
  65. package/dist/utils/stack-frames.d.ts +50 -0
  66. package/dist/utils/stack-frames.d.ts.map +1 -0
  67. package/dist/utils/stack-frames.js +64 -0
  68. package/dist/utils/stack-frames.js.map +1 -0
  69. package/dist/vite-plugin/changes-route.d.ts +10 -0
  70. package/dist/vite-plugin/changes-route.d.ts.map +1 -0
  71. package/dist/vite-plugin/changes-route.js +174 -0
  72. package/dist/vite-plugin/changes-route.js.map +1 -0
  73. package/dist/vite-plugin/index.d.ts +7 -0
  74. package/dist/vite-plugin/index.d.ts.map +1 -1
  75. package/dist/vite-plugin/index.js +38 -20
  76. package/dist/vite-plugin/index.js.map +1 -1
  77. package/dist/vite-plugin/route-header.d.ts +10 -0
  78. package/dist/vite-plugin/route-header.d.ts.map +1 -0
  79. package/dist/vite-plugin/route-header.js +14 -0
  80. package/dist/vite-plugin/route-header.js.map +1 -0
  81. package/dist/vite-plugin/sveltekit-generator.d.ts +7 -0
  82. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  83. package/dist/vite-plugin/sveltekit-generator.js +46 -20
  84. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  85. package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
  86. package/dist/vite-plugin/sync-apply-route.js +2 -2
  87. package/dist/vite-plugin/sync-apply-route.js.map +1 -1
  88. package/package.json +4 -4
@@ -39,6 +39,17 @@ export declare class APIGenerator {
39
39
  * @param collection - Pre-initialized SmrtCollection instance
40
40
  */
41
41
  registerCollection(name: string, collection: SmrtCollection<SmrtObject>): void;
42
+ /**
43
+ * Resolve the database for generator-level routes that are not object-scoped
44
+ * (currently the `_changes` feed, #1758).
45
+ *
46
+ * Prefers the explicit `APIContext.db`, but falls back to the first
47
+ * registered collection's live handle so the feed works under the documented
48
+ * `registerCollection()` wiring, where callers configure collections with a
49
+ * database but never populate `APIContext.db`. Without the fallback the feed
50
+ * would 503 even though the database is reachable.
51
+ */
52
+ private resolveContextDb;
42
53
  /**
43
54
  * Create Node.js HTTP server with all routes
44
55
  */
@@ -85,6 +96,36 @@ export declare class APIGenerator {
85
96
  private isRoutePublic;
86
97
  private isApiActionEnabled;
87
98
  private getCollectionObjectName;
99
+ /**
100
+ * Fail-closed tenant read scope (#1782).
101
+ *
102
+ * A `@TenantScoped` model served over a public/anonymous read has no ambient
103
+ * tenant context, so the tenancy interceptor (optional mode) passes the query
104
+ * through UNFILTERED and returns every tenant's rows. When tenancy is enabled
105
+ * but no tenant is active, this returns a `{ tenantId: null }` filter so reads
106
+ * fail closed to NULL-tenant (global) rows only — mirroring the dispatch
107
+ * resolver's "enforced, no active tenant → global rows only" convention
108
+ * (`resolveDispatchTenantScope` is the core-level trust anchor tenancy fills
109
+ * in; core cannot import tenancy directly). Returns undefined when a tenant IS
110
+ * active (the interceptor filters by it) or tenancy is disabled (no isolation
111
+ * to enforce).
112
+ *
113
+ * Tenant scoping is recognized across BOTH registration forms (#1782): the
114
+ * `@smrt({ tenantScoped })` config / manifest-merged form via
115
+ * `ObjectRegistry.isTenantScoped`, and the standalone `@TenantScoped()`
116
+ * decorator via the tenancy-filled `isTenantScopedClassResolved` hook — so a
117
+ * `@TenantScoped()`-only public model can't slip past the guard regardless of
118
+ * manifest timing.
119
+ */
120
+ private resolveTenantReadScope;
121
+ /**
122
+ * Merge the fail-closed tenant read scope (#1782) into a query's WHERE clause.
123
+ * When the scope is active (global-only), any client-supplied tenant filter is
124
+ * dropped first so a `?tenantId=...` / `?tenant_id[ne]=...` query param can
125
+ * never widen the scope, then NULL-tenant is forced. Returns the original
126
+ * `where` untouched when the scope is inactive.
127
+ */
128
+ private applyTenantReadScope;
88
129
  /**
89
130
  * Handle GET /objects/:id
90
131
  */
@@ -168,6 +209,23 @@ export declare class APIGenerator {
168
209
  * Add CORS headers to response
169
210
  */
170
211
  private addCorsHeaders;
212
+ /**
213
+ * Resolve a URL/collection segment to a registered class via auto-discovery.
214
+ *
215
+ * The segment IS the canonical `collection` value from the manifest
216
+ * (`RegisteredClass.collection`, e.g. `products` for `Product`) — the same
217
+ * value the generated client, the SvelteKit route generator, and the registry
218
+ * all derive. Match it VERBATIM; do not re-pluralize. The previous
219
+ * implementation pluralized both the URL segment and the class name and
220
+ * compared them, turning an already-plural client URL (`/products`) into
221
+ * `productses`, so the generated client 404'd against auto-discovered routes
222
+ * (#1794). Shared by the CRUD path and the `sync/apply` batch path so they can
223
+ * never drift.
224
+ *
225
+ * @param segment - The URL/collection path segment (e.g. `products`).
226
+ * @returns The matching registered class, or null if none matches.
227
+ */
228
+ private findClassByCollectionSegment;
171
229
  /**
172
230
  * Simple pluralization (basic implementation)
173
231
  */
@@ -1 +1 @@
1
- {"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../../src/generators/rest.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../../src/generators/rest.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,9 @@
1
+ import { isTenantScopedClassResolved, resolveDispatchTenantScope } from "../dispatch/tenant-resolver.js";
1
2
  import { ObjectRegistry } from "../registry.js";
2
- import { conditionalJsonResponse, resolveReadCacheControl, warnIfSharedCacheNeutralized } from "./conditional-get.js";
3
+ import "../dispatch/index.js";
4
+ import { conditionalJsonResponse, resolveReadCacheControl, warnIfSharedCacheNeutralized, warnIfTenantScopedPublicRead } from "./conditional-get.js";
3
5
  import { SYNC_APPLY_ROUTE_SEGMENTS, processSyncApplyBatch } from "../sync/apply.js";
6
+ import { handleChangesRoute } from "./changes-route.js";
4
7
  import http from "node:http";
5
8
  //#region src/generators/rest.ts
6
9
  /**
@@ -35,6 +38,19 @@ var APIGenerator = class {
35
38
  this.collections.set(name, collection);
36
39
  }
37
40
  /**
41
+ * Resolve the database for generator-level routes that are not object-scoped
42
+ * (currently the `_changes` feed, #1758).
43
+ *
44
+ * Prefers the explicit `APIContext.db`, but falls back to the first
45
+ * registered collection's live handle so the feed works under the documented
46
+ * `registerCollection()` wiring, where callers configure collections with a
47
+ * database but never populate `APIContext.db`. Without the fallback the feed
48
+ * would 503 even though the database is reachable.
49
+ */
50
+ resolveContextDb() {
51
+ return this.context.db ?? this.collections.values().next().value?.db ?? void 0;
52
+ }
53
+ /**
38
54
  * Create Node.js HTTP server with all routes
39
55
  */
40
56
  createServer() {
@@ -114,6 +130,13 @@ var APIGenerator = class {
114
130
  return this.addCorsHeaders(response, req);
115
131
  }
116
132
  }
133
+ if (url.pathname === `${this.config.basePath}/_changes`) {
134
+ const response = await handleChangesRoute(req, {
135
+ authMiddleware: this.config.authMiddleware,
136
+ db: this.resolveContextDb()
137
+ });
138
+ return this.addCorsHeaders(response, req);
139
+ }
117
140
  if (url.pathname.startsWith(this.config.basePath || "")) {
118
141
  const response = await this.handleObjectRoute(req, url);
119
142
  return this.addCorsHeaders(response, req);
@@ -143,20 +166,14 @@ var APIGenerator = class {
143
166
  } else if (!this.isRoutePublic(objectName, req.method)) return this.createErrorResponse(401, "Authentication required");
144
167
  return await this.executeCrudOperation(req, collection, objectId, url, objectName);
145
168
  }
146
- const registeredClasses = ObjectRegistry.getAllClasses();
147
- const pluralName = this.pluralize(objectType);
148
- let classInfo = null;
149
- for (const [_key, info] of registeredClasses) if (this.pluralize((info.name || _key).toLowerCase()) === pluralName) {
150
- classInfo = info;
151
- break;
152
- }
169
+ const classInfo = this.findClassByCollectionSegment(objectType);
153
170
  if (!classInfo) return this.createErrorResponse(404, `Object type '${objectType}' not found`);
154
171
  if (this.config.authMiddleware) {
155
172
  const authResult = await this.config.authMiddleware(classInfo.name, req.method.toLowerCase())(req);
156
173
  if (authResult instanceof Response) return authResult;
157
174
  req = authResult;
158
175
  } else if (!this.isRoutePublic(classInfo.name, req.method)) return this.createErrorResponse(401, "Authentication required");
159
- const collection = this.getCollection(classInfo);
176
+ const collection = await this.getCollection(classInfo);
160
177
  return await this.executeCrudOperation(req, collection, objectId, url, classInfo.name);
161
178
  }
162
179
  /**
@@ -166,7 +183,7 @@ var APIGenerator = class {
166
183
  try {
167
184
  const action = this.getCrudAction(req.method, objectId);
168
185
  if (action && !this.isApiActionEnabled(objectName, action)) return this.createErrorResponse(405, "Method not allowed");
169
- if (objectId === "count" && req.method === "GET") return await this.handleCount(collection, url.searchParams);
186
+ if (objectId === "count" && req.method === "GET") return await this.handleCount(collection, url.searchParams, objectName);
170
187
  switch (req.method) {
171
188
  case "GET": return objectId ? await this.handleGet(collection, objectId, req, objectName) : await this.handleList(collection, url.searchParams, req, objectName);
172
189
  case "POST": return await this.handleCreate(collection, req, objectName);
@@ -227,10 +244,58 @@ var APIGenerator = class {
227
244
  return registered?.qualifiedName || registered?.name || itemClass.name;
228
245
  }
229
246
  /**
247
+ * Fail-closed tenant read scope (#1782).
248
+ *
249
+ * A `@TenantScoped` model served over a public/anonymous read has no ambient
250
+ * tenant context, so the tenancy interceptor (optional mode) passes the query
251
+ * through UNFILTERED and returns every tenant's rows. When tenancy is enabled
252
+ * but no tenant is active, this returns a `{ tenantId: null }` filter so reads
253
+ * fail closed to NULL-tenant (global) rows only — mirroring the dispatch
254
+ * resolver's "enforced, no active tenant → global rows only" convention
255
+ * (`resolveDispatchTenantScope` is the core-level trust anchor tenancy fills
256
+ * in; core cannot import tenancy directly). Returns undefined when a tenant IS
257
+ * active (the interceptor filters by it) or tenancy is disabled (no isolation
258
+ * to enforce).
259
+ *
260
+ * Tenant scoping is recognized across BOTH registration forms (#1782): the
261
+ * `@smrt({ tenantScoped })` config / manifest-merged form via
262
+ * `ObjectRegistry.isTenantScoped`, and the standalone `@TenantScoped()`
263
+ * decorator via the tenancy-filled `isTenantScopedClassResolved` hook — so a
264
+ * `@TenantScoped()`-only public model can't slip past the guard regardless of
265
+ * manifest timing.
266
+ */
267
+ resolveTenantReadScope(objectName) {
268
+ if (!objectName || !(ObjectRegistry.isTenantScoped(objectName) || !!ObjectRegistry.getConfig(objectName)?.tenantScoped || isTenantScopedClassResolved(objectName))) return;
269
+ const scope = resolveDispatchTenantScope();
270
+ return scope.enforced && scope.tenantId === null ? { tenantId: null } : void 0;
271
+ }
272
+ /**
273
+ * Merge the fail-closed tenant read scope (#1782) into a query's WHERE clause.
274
+ * When the scope is active (global-only), any client-supplied tenant filter is
275
+ * dropped first so a `?tenantId=...` / `?tenant_id[ne]=...` query param can
276
+ * never widen the scope, then NULL-tenant is forced. Returns the original
277
+ * `where` untouched when the scope is inactive.
278
+ */
279
+ applyTenantReadScope(objectName, where) {
280
+ if (!this.resolveTenantReadScope(objectName)) return where;
281
+ const cleaned = {};
282
+ for (const [key, value] of Object.entries(where ?? {})) {
283
+ const field = key.split(/\s+/)[0];
284
+ if (field === "tenantId" || field === "tenant_id") continue;
285
+ cleaned[key] = value;
286
+ }
287
+ cleaned.tenantId = null;
288
+ return cleaned;
289
+ }
290
+ /**
230
291
  * Handle GET /objects/:id
231
292
  */
232
293
  async handleGet(collection, id, req, objectName) {
233
- const object = await collection.get(id);
294
+ const scope = this.resolveTenantReadScope(objectName);
295
+ const object = scope ? await collection.get({
296
+ id,
297
+ ...scope
298
+ }) : await collection.get(id);
234
299
  if (!object) return this.createErrorResponse(404, "Object not found");
235
300
  return this.createReadResponse(req, objectName, this.toPublicData(object));
236
301
  }
@@ -263,8 +328,9 @@ var APIGenerator = class {
263
328
  where[sqlKey] = operator === "in" ? value.split(",") : value;
264
329
  } else where[key] = value;
265
330
  }
331
+ const scopedWhere = this.applyTenantReadScope(objectName, where);
266
332
  const objects = await collection.list({
267
- where: Object.keys(where).length > 0 ? where : void 0,
333
+ where: scopedWhere && Object.keys(scopedWhere).length > 0 ? scopedWhere : void 0,
268
334
  limit,
269
335
  offset,
270
336
  orderBy
@@ -274,7 +340,7 @@ var APIGenerator = class {
274
340
  /**
275
341
  * Handle GET /objects/count
276
342
  */
277
- async handleCount(collection, params) {
343
+ async handleCount(collection, params, objectName) {
278
344
  const where = {};
279
345
  for (const [key, value] of params.entries()) {
280
346
  const match = key.match(/^(.+)\[(.+)\]$/);
@@ -293,7 +359,8 @@ var APIGenerator = class {
293
359
  where[sqlKey] = operator === "in" ? value.split(",") : value;
294
360
  } else where[key] = value;
295
361
  }
296
- const count = await collection.count({ where: Object.keys(where).length > 0 ? where : void 0 });
362
+ const scopedWhere = this.applyTenantReadScope(objectName, where);
363
+ const count = await collection.count({ where: scopedWhere && Object.keys(scopedWhere).length > 0 ? scopedWhere : void 0 });
297
364
  return this.createJsonResponse({ count });
298
365
  }
299
366
  /**
@@ -351,7 +418,7 @@ var APIGenerator = class {
351
418
  * resolves a CRUD URL segment (registered collections first, then registry
352
419
  * auto-discovery), and wrap the generator's per-object machinery.
353
420
  */
354
- resolveSyncApplyTarget(objectSegment, req, rawBody) {
421
+ async resolveSyncApplyTarget(objectSegment, req, rawBody) {
355
422
  let collection = null;
356
423
  let objectName = null;
357
424
  const registered = this.collections.get(objectSegment);
@@ -359,11 +426,10 @@ var APIGenerator = class {
359
426
  collection = registered;
360
427
  objectName = this.getCollectionObjectName(registered) || objectSegment;
361
428
  } else {
362
- const pluralName = this.pluralize(objectSegment);
363
- for (const [key, info] of ObjectRegistry.getAllClasses()) if (this.pluralize((info.name || key).toLowerCase()) === pluralName) {
364
- collection = this.getCollection(info);
429
+ const info = this.findClassByCollectionSegment(objectSegment);
430
+ if (info) {
431
+ collection = await this.getCollection(info);
365
432
  objectName = info.name;
366
- break;
367
433
  }
368
434
  }
369
435
  if (!collection || !objectName) return null;
@@ -391,13 +457,14 @@ var APIGenerator = class {
391
457
  /**
392
458
  * Get or create collection instance
393
459
  */
394
- getCollection(classInfo) {
460
+ async getCollection(classInfo) {
395
461
  if (!this.collections.has(classInfo.name)) {
396
462
  const ctor = classInfo.collectionConstructor;
397
463
  const collection = new ctor({
398
464
  ai: this.context.ai,
399
465
  db: this.context.db
400
466
  });
467
+ await collection.initialize();
401
468
  this.collections.set(classInfo.name, collection);
402
469
  }
403
470
  const collection = this.collections.get(classInfo.name);
@@ -457,7 +524,10 @@ var APIGenerator = class {
457
524
  const config = objectName ? ObjectRegistry.getConfig(objectName) : void 0;
458
525
  const apiConfig = config?.api;
459
526
  const tenantScoped = objectName ? ObjectRegistry.isTenantScoped(objectName) || !!config?.tenantScoped : false;
460
- if (objectName) warnIfSharedCacheNeutralized(objectName, apiConfig, tenantScoped);
527
+ if (objectName) {
528
+ warnIfSharedCacheNeutralized(objectName, apiConfig, tenantScoped);
529
+ warnIfTenantScopedPublicRead(objectName, apiConfig, tenantScoped);
530
+ }
461
531
  return conditionalJsonResponse(req, payload, resolveReadCacheControl(apiConfig, { tenantScoped }));
462
532
  }
463
533
  /**
@@ -527,6 +597,29 @@ var APIGenerator = class {
527
597
  });
528
598
  }
529
599
  /**
600
+ * Resolve a URL/collection segment to a registered class via auto-discovery.
601
+ *
602
+ * The segment IS the canonical `collection` value from the manifest
603
+ * (`RegisteredClass.collection`, e.g. `products` for `Product`) — the same
604
+ * value the generated client, the SvelteKit route generator, and the registry
605
+ * all derive. Match it VERBATIM; do not re-pluralize. The previous
606
+ * implementation pluralized both the URL segment and the class name and
607
+ * compared them, turning an already-plural client URL (`/products`) into
608
+ * `productses`, so the generated client 404'd against auto-discovered routes
609
+ * (#1794). Shared by the CRUD path and the `sync/apply` batch path so they can
610
+ * never drift.
611
+ *
612
+ * @param segment - The URL/collection path segment (e.g. `products`).
613
+ * @returns The matching registered class, or null if none matches.
614
+ */
615
+ findClassByCollectionSegment(segment) {
616
+ for (const [key, info] of ObjectRegistry.getAllClasses()) {
617
+ if (info.collection === segment) return info;
618
+ if (!info.collection && this.pluralize((info.name || key).toLowerCase()) === segment) return info;
619
+ }
620
+ return null;
621
+ }
622
+ /**
530
623
  * Simple pluralization (basic implementation)
531
624
  */
532
625
  pluralize(word) {
@@ -1 +1 @@
1
- {"version":3,"file":"rest.js","names":[],"sources":["../../src/generators/rest.ts"],"sourcesContent":["/**\n * High-performance REST API generator for smrt objects using Node.js HTTP server\n *\n * Designed for minimal bundle size and maximum performance\n */\n\nimport http from 'node:http';\nimport type { SmrtCollection } from '../collection';\nimport type { SmrtObject } from '../object';\nimport { ObjectRegistry } from '../registry';\nimport type { RegisteredClass, SmrtObjectConstructor } from '../registry/types';\nimport {\n conditionalJsonResponse,\n resolveReadCacheControl,\n warnIfSharedCacheNeutralized,\n} from './conditional-get';\nimport {\n processSyncApplyBatch,\n SYNC_APPLY_ROUTE_SEGMENTS,\n type SyncApplyOp,\n type SyncApplyTarget,\n} from '../sync/apply';\n\nexport interface APIConfig {\n basePath?: string;\n enableCors?: boolean;\n /**\n * Explicit CORS origin allowlist (#1540). Required when `enableCors` is true —\n * the generator never emits `Access-Control-Allow-Origin: *`. A request's\n * `Origin` is echoed only when it appears here.\n */\n allowedOrigins?: string[];\n customRoutes?: Record<string, (req: Request) => Promise<Response>>;\n authMiddleware?: (\n objectName: string,\n action: string,\n ) => (req: Request) => Promise<Request | Response>;\n port?: number;\n hostname?: string;\n}\n\nexport interface APIContext {\n db?: unknown;\n ai?: unknown;\n user?: {\n id: string;\n username?: string;\n roles?: string[];\n };\n}\n\n/**\n * High-performance API generator using native Bun\n */\nexport class APIGenerator {\n private config: APIConfig;\n private collections = new Map<string, SmrtCollection<SmrtObject>>();\n private context: APIContext;\n\n constructor(config: APIConfig = {}, context: APIContext = {}) {\n this.config = {\n basePath: '/api/v1',\n // Security defaults (#1540): bind to loopback and keep CORS off unless an\n // explicit origin allowlist is supplied. No `Access-Control-Allow-Origin: *`.\n enableCors: false,\n port: 3000,\n hostname: '127.0.0.1',\n ...config,\n };\n this.context = context;\n }\n\n /**\n * Register a pre-configured collection instance for API exposure\n *\n * @param name - URL path segment for the collection (e.g., 'products' for /api/products)\n * @param collection - Pre-initialized SmrtCollection instance\n */\n registerCollection(\n name: string,\n collection: SmrtCollection<SmrtObject>,\n ): void {\n this.collections.set(name, collection);\n }\n\n /**\n * Create Node.js HTTP server with all routes\n */\n createServer(): { server: http.Server; url: string } {\n const server = http.createServer(async (req, res) => {\n try {\n const request = await this.nodeRequestToWebRequest(req);\n const response = await this.handleRequest(request);\n await this.webResponseToNodeResponse(response, res);\n } catch (_error) {\n res.statusCode = 500;\n res.end('Internal Server Error');\n }\n });\n\n server.listen(this.config.port, this.config.hostname);\n\n return {\n server,\n url: `http://${this.config.hostname}:${this.config.port}`,\n };\n }\n\n /**\n * Convert stream to string\n */\n private async streamToString(stream: http.IncomingMessage): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n }\n return Buffer.concat(chunks).toString('utf-8');\n }\n\n /**\n * Convert Node.js IncomingMessage to Web Request\n */\n private async nodeRequestToWebRequest(\n req: http.IncomingMessage,\n ): Promise<Request> {\n const url = `http://${this.config.hostname}:${this.config.port}${req.url}`;\n const method = req.method || 'GET';\n const headers = new Headers();\n\n for (const [key, value] of Object.entries(req.headers)) {\n if (value) {\n headers.set(key, Array.isArray(value) ? value[0] : value);\n }\n }\n\n let body: string | undefined;\n if (method !== 'GET' && method !== 'HEAD') {\n body = await this.streamToString(req);\n }\n\n return new Request(url, {\n method,\n headers,\n body,\n });\n }\n\n /**\n * Convert Web Response to Node.js ServerResponse\n */\n private async webResponseToNodeResponse(\n webResponse: Response,\n res: http.ServerResponse,\n ): Promise<void> {\n res.statusCode = webResponse.status;\n\n // Set headers\n webResponse.headers.forEach((value, key) => {\n res.setHeader(key, value);\n });\n\n // Send body\n if (webResponse.body) {\n const reader = webResponse.body.getReader();\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n res.write(value);\n }\n }\n\n res.end();\n }\n\n /**\n * Generate fetch handler function (for serverless environments)\n */\n generateHandler(): (req: Request) => Promise<Response> {\n return (req) => this.handleRequest(req);\n }\n\n /**\n * Main request handler using native Bun APIs\n */\n private async handleRequest(req: Request): Promise<Response> {\n const url = new URL(req.url);\n\n // Handle CORS preflight\n if (req.method === 'OPTIONS' && this.config.enableCors) {\n return this.createCorsResponse(req);\n }\n\n // Handle custom routes first\n if (this.config.customRoutes) {\n for (const [path, handler] of Object.entries(this.config.customRoutes)) {\n if (url.pathname === `${this.config.basePath}${path}`) {\n const response = await handler(req);\n return this.addCorsHeaders(response, req);\n }\n }\n }\n\n // Handle object routes\n if (url.pathname.startsWith(this.config.basePath || '')) {\n const response = await this.handleObjectRoute(req, url);\n return this.addCorsHeaders(response, req);\n }\n\n // Not found\n return this.createErrorResponse(404, 'Not found');\n }\n\n /**\n * Handle CRUD routes for SMRT objects\n */\n private async handleObjectRoute(req: Request, url: URL): Promise<Response> {\n const pathParts = url.pathname\n .replace(this.config.basePath || '', '')\n .split('/')\n .filter(Boolean);\n\n if (pathParts.length === 0) {\n return this.createErrorResponse(400, 'Object type required');\n }\n\n // Reserved batch write contract route (#1759): POST {basePath}/sync/apply.\n if (\n pathParts.length === SYNC_APPLY_ROUTE_SEGMENTS.length &&\n SYNC_APPLY_ROUTE_SEGMENTS.every((segment, i) => pathParts[i] === segment)\n ) {\n if (req.method !== 'POST') {\n return this.createErrorResponse(405, 'Method not allowed');\n }\n return await this.handleSyncApply(req);\n }\n\n const objectType = pathParts[0];\n const objectId = pathParts[1];\n\n // Check for explicitly registered collection first\n if (this.collections.has(objectType)) {\n const collection = this.collections.get(objectType);\n if (!collection) throw new Error(`Collection ${objectType} not found`);\n\n const objectName = this.getCollectionObjectName(collection) || objectType;\n\n // Apply auth middleware if configured\n if (this.config.authMiddleware) {\n const authCheck = this.config.authMiddleware(\n objectName,\n req.method.toLowerCase(),\n );\n const authResult = await authCheck(req);\n if (authResult instanceof Response) {\n return authResult; // Auth failed\n }\n // Auth passed, use the potentially modified request\n req = authResult;\n } else if (!this.isRoutePublic(objectName, req.method)) {\n // Fail-closed (#1540): no auth middleware wired and the object isn't\n // marked `@smrt({ api: { public } })` → refuse rather than serve open.\n return this.createErrorResponse(401, 'Authentication required');\n }\n\n // Use registered collection directly\n return await this.executeCrudOperation(\n req,\n collection,\n objectId,\n url,\n objectName,\n );\n }\n\n // Fall back to auto-discovery via ObjectRegistry\n const registeredClasses = ObjectRegistry.getAllClasses();\n const pluralName = this.pluralize(objectType);\n\n let classInfo: RegisteredClass | null = null;\n for (const [_key, info] of registeredClasses) {\n // Issue #951: Use simple name (info.name) for URL matching, not the map key\n // which may be a qualified name like '@happyvertical/smrt-events:Event'\n if (this.pluralize((info.name || _key).toLowerCase()) === pluralName) {\n classInfo = info;\n break;\n }\n }\n\n if (!classInfo) {\n return this.createErrorResponse(\n 404,\n `Object type '${objectType}' not found`,\n );\n }\n\n // Apply auth middleware if configured\n if (this.config.authMiddleware) {\n const authCheck = this.config.authMiddleware(\n classInfo.name,\n req.method.toLowerCase(),\n );\n const authResult = await authCheck(req);\n if (authResult instanceof Response) {\n return authResult; // Auth failed\n }\n // Auth passed, use the potentially modified request\n req = authResult;\n } else if (!this.isRoutePublic(classInfo.name, req.method)) {\n // Fail-closed (#1540): see registered-collection branch above.\n return this.createErrorResponse(401, 'Authentication required');\n }\n\n // Get or create collection\n const collection = this.getCollection(classInfo);\n\n return await this.executeCrudOperation(\n req,\n collection,\n objectId,\n url,\n classInfo.name,\n );\n }\n\n /**\n * Execute CRUD operation on a collection\n */\n private async executeCrudOperation(\n req: Request,\n collection: SmrtCollection<SmrtObject>,\n objectId: string | undefined,\n url: URL,\n objectName?: string,\n ): Promise<Response> {\n try {\n const action = this.getCrudAction(req.method, objectId);\n if (action && !this.isApiActionEnabled(objectName, action)) {\n return this.createErrorResponse(405, 'Method not allowed');\n }\n\n // Handle special /count endpoint\n if (objectId === 'count' && req.method === 'GET') {\n return await this.handleCount(collection, url.searchParams);\n }\n\n // Route to appropriate CRUD operation\n switch (req.method) {\n case 'GET':\n return objectId\n ? await this.handleGet(collection, objectId, req, objectName)\n : await this.handleList(\n collection,\n url.searchParams,\n req,\n objectName,\n );\n\n case 'POST':\n return await this.handleCreate(collection, req, objectName);\n\n case 'PUT':\n case 'PATCH':\n if (!objectId) {\n return this.createErrorResponse(\n 400,\n 'Object ID required for update',\n );\n }\n return await this.handleUpdate(collection, objectId, req, objectName);\n\n case 'DELETE':\n if (!objectId) {\n return this.createErrorResponse(\n 400,\n 'Object ID required for delete',\n );\n }\n return await this.handleDelete(collection, objectId);\n\n default:\n return this.createErrorResponse(405, 'Method not allowed');\n }\n } catch (error) {\n console.error('API Error:', error);\n return this.createErrorResponse(500, 'Internal server error');\n }\n }\n\n private getCrudAction(\n method: string,\n objectId: string | undefined,\n ): 'list' | 'get' | 'create' | 'update' | 'delete' | null {\n switch (method) {\n case 'GET':\n return objectId && objectId !== 'count' ? 'get' : 'list';\n case 'POST':\n return 'create';\n case 'PUT':\n case 'PATCH':\n return 'update';\n case 'DELETE':\n return 'delete';\n default:\n return null;\n }\n }\n\n /**\n * Fail-closed authorization posture (#1540). Returns true only when the\n * object opts out of auth via `@smrt({ api: { public } })`:\n * - `public: true` → all methods are public.\n * - `public: 'read'` → only safe (GET) methods are public.\n * Everything else requires an `authMiddleware` to be configured.\n */\n private isRoutePublic(\n objectName: string | undefined,\n method: string,\n ): boolean {\n if (!objectName) return false;\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api;\n if (!apiConfig || typeof apiConfig !== 'object') return false;\n const publicAccess = (apiConfig as { public?: boolean | 'read' }).public;\n if (publicAccess === true) return true;\n if (publicAccess === 'read') return method.toUpperCase() === 'GET';\n return false;\n }\n\n private isApiActionEnabled(\n objectName: string | undefined,\n action: 'list' | 'get' | 'create' | 'update' | 'delete',\n ): boolean {\n if (!objectName) {\n return true;\n }\n\n const config = ObjectRegistry.getConfig(objectName);\n const apiConfig = config.api;\n\n if (apiConfig === false) {\n return false;\n }\n\n if (apiConfig && typeof apiConfig === 'object') {\n if (apiConfig.include && !apiConfig.include.includes(action)) {\n return false;\n }\n\n if (apiConfig.exclude?.includes(action)) {\n return false;\n }\n }\n\n return true;\n }\n\n private getCollectionObjectName(\n collection: SmrtCollection<SmrtObject>,\n ): string | undefined {\n // `_itemClass` lives on the instance (protected getter) or the collection\n // constructor (static). Read it through a structural shape rather than the\n // class's private surface; behavior is unchanged.\n const itemClass: SmrtObjectConstructor | undefined =\n (collection as unknown as { _itemClass?: SmrtObjectConstructor })\n ._itemClass ||\n (\n collection.constructor as unknown as {\n _itemClass?: SmrtObjectConstructor;\n }\n )?._itemClass;\n\n if (!itemClass) {\n return undefined;\n }\n\n const registered = ObjectRegistry.getClassByConstructor(itemClass);\n return registered?.qualifiedName || registered?.name || itemClass.name;\n }\n\n /**\n * Handle GET /objects/:id\n */\n private async handleGet(\n collection: SmrtCollection<SmrtObject>,\n id: string,\n req: Request,\n objectName?: string,\n ): Promise<Response> {\n const object = await collection.get(id);\n if (!object) {\n return this.createErrorResponse(404, 'Object not found');\n }\n return this.createReadResponse(req, objectName, this.toPublicData(object));\n }\n\n /**\n * Handle GET /objects (list with query params)\n */\n private async handleList(\n collection: SmrtCollection<SmrtObject>,\n params: URLSearchParams,\n req: Request,\n objectName?: string,\n ): Promise<Response> {\n const limit = Number.parseInt(params.get('limit') || '50', 10);\n const offset = Number.parseInt(params.get('offset') || '0', 10);\n const orderBy = params.get('orderBy') || 'created_at DESC';\n\n // Build where clause from query params\n // Convert REST-style operators (price[gt]) to SQL-style (price >)\n const where: Record<string, string | string[]> = {};\n for (const [key, value] of params.entries()) {\n if (!['limit', 'offset', 'orderBy'].includes(key)) {\n // Parse REST operator format: field[operator]\n const match = key.match(/^(.+)\\[(.+)\\]$/);\n if (match) {\n const field = match[1];\n const operator = match[2];\n // Map REST operators to SQL operators\n const operatorMap: Record<string, string> = {\n gt: '>',\n gte: '>=',\n lt: '<',\n lte: '<=',\n ne: '!=',\n in: 'in',\n like: 'like',\n };\n const sqlOperator = operatorMap[operator] || operator;\n const sqlKey = `${field} ${sqlOperator}`;\n // Handle 'in' operator - convert comma-separated string to array\n where[sqlKey] = operator === 'in' ? value.split(',') : value;\n } else {\n where[key] = value;\n }\n }\n }\n\n const objects = await collection.list({\n where: Object.keys(where).length > 0 ? where : undefined,\n limit,\n offset,\n orderBy,\n });\n\n return this.createReadResponse(\n req,\n objectName,\n objects.map((object: SmrtObject) => this.toPublicData(object)),\n );\n }\n\n /**\n * Handle GET /objects/count\n */\n private async handleCount(\n collection: SmrtCollection<SmrtObject>,\n params: URLSearchParams,\n ): Promise<Response> {\n // Build where clause from query params (same logic as handleList)\n const where: Record<string, string | string[]> = {};\n for (const [key, value] of params.entries()) {\n // Parse REST operator format: field[operator]\n const match = key.match(/^(.+)\\[(.+)\\]$/);\n if (match) {\n const field = match[1];\n const operator = match[2];\n // Map REST operators to SQL operators\n const operatorMap: Record<string, string> = {\n gt: '>',\n gte: '>=',\n lt: '<',\n lte: '<=',\n ne: '!=',\n in: 'in',\n like: 'like',\n };\n const sqlOperator = operatorMap[operator] || operator;\n const sqlKey = `${field} ${sqlOperator}`;\n // Handle 'in' operator - convert comma-separated string to array\n where[sqlKey] = operator === 'in' ? value.split(',') : value;\n } else {\n where[key] = value;\n }\n }\n\n const count = await collection.count({\n where: Object.keys(where).length > 0 ? where : undefined,\n });\n\n return this.createJsonResponse({ count });\n }\n\n /**\n * Handle POST /objects\n */\n private async handleCreate(\n collection: SmrtCollection<SmrtObject>,\n req: Request,\n objectName?: string,\n ): Promise<Response> {\n const data = this.applyWritablePolicy(objectName, await req.json());\n const object = await collection.create({ ...data, _skipLoad: true });\n await object.save();\n return this.createJsonResponse(this.toPublicData(object), 201);\n }\n\n /**\n * Handle PUT/PATCH /objects/:id\n */\n private async handleUpdate(\n collection: SmrtCollection<SmrtObject>,\n id: string,\n req: Request,\n objectName?: string,\n ): Promise<Response> {\n const data = this.applyWritablePolicy(objectName, await req.json());\n const object = await collection.get(id);\n\n if (!object) {\n return this.createErrorResponse(404, 'Object not found');\n }\n\n // Update object properties\n Object.assign(object, data);\n await object.save();\n\n return this.createJsonResponse(this.toPublicData(object));\n }\n\n /**\n * Handle DELETE /objects/:id\n */\n private async handleDelete(\n collection: SmrtCollection<SmrtObject>,\n id: string,\n ): Promise<Response> {\n const object = await collection.get(id);\n\n if (!object) {\n return this.createErrorResponse(404, 'Object not found');\n }\n\n await object.delete();\n return new Response(null, { status: 204 });\n }\n\n /**\n * Handle POST /sync/apply — the idempotent batch write contract (#1759).\n * All processing lives in `sync/apply.ts`; this method only adapts the\n * generator's existing collection resolution, auth, action gating, and\n * writable policy into a {@link SyncApplyTarget} per item.\n */\n private async handleSyncApply(req: Request): Promise<Response> {\n // Buffer the body once. Request bodies are single-read streams, and the\n // per-item auth middleware below must receive a readable body exactly as\n // it does on CRUD routes (where auth runs before parsing) — so each\n // middleware invocation gets a fresh Request rebuilt from this buffer.\n let rawBody: string;\n let body: unknown;\n try {\n rawBody = await req.text();\n body = JSON.parse(rawBody);\n } catch {\n return this.createErrorResponse(400, 'Invalid JSON body');\n }\n\n const outcome = await processSyncApplyBatch(body, {\n resolveTarget: (objectSegment) =>\n this.resolveSyncApplyTarget(objectSegment, req, rawBody),\n });\n return this.createJsonResponse(outcome.body, outcome.status);\n }\n\n /**\n * Resolve a sync item's `object` segment exactly like `handleObjectRoute`\n * resolves a CRUD URL segment (registered collections first, then registry\n * auto-discovery), and wrap the generator's per-object machinery.\n */\n private resolveSyncApplyTarget(\n objectSegment: string,\n req: Request,\n rawBody: string,\n ): SyncApplyTarget | null {\n let collection: SmrtCollection<SmrtObject> | null = null;\n let objectName: string | null = null;\n\n const registered = this.collections.get(objectSegment);\n if (registered) {\n collection = registered;\n objectName = this.getCollectionObjectName(registered) || objectSegment;\n } else {\n const pluralName = this.pluralize(objectSegment);\n for (const [key, info] of ObjectRegistry.getAllClasses()) {\n if (this.pluralize((info.name || key).toLowerCase()) === pluralName) {\n collection = this.getCollection(info);\n objectName = info.name;\n break;\n }\n }\n }\n\n if (!collection || !objectName) {\n return null;\n }\n\n const resolvedName = objectName;\n const resolvedCollection = collection;\n return {\n objectName: resolvedName,\n collection: resolvedCollection,\n isOpAllowed: (op: SyncApplyOp) =>\n this.isApiActionEnabled(resolvedName, op),\n authorize: async (op: SyncApplyOp) => {\n if (this.config.authMiddleware) {\n const verb =\n op === 'create' ? 'post' : op === 'update' ? 'put' : 'delete';\n const authCheck = this.config.authMiddleware(resolvedName, verb);\n // Fresh Request per invocation: the batch handler already consumed\n // the original body, and middleware may read it (body-based auth,\n // request signing) exactly as it can on CRUD routes.\n const authResult = await authCheck(\n new Request(req.url, {\n method: req.method,\n headers: req.headers,\n body: rawBody,\n }),\n );\n if (authResult instanceof Response) {\n return authResult.status === 401 ? 'auth_required' : 'forbidden';\n }\n return 'ok';\n }\n // Fail-closed (#1540): every sync op is mutating, so only\n // `@smrt({ api: { public: true } })` objects apply without auth.\n return this.isRoutePublic(resolvedName, 'POST')\n ? 'ok'\n : 'auth_required';\n },\n prepare: (payload) => this.applyWritablePolicy(resolvedName, payload),\n };\n }\n\n /**\n * Get or create collection instance\n */\n private getCollection(\n classInfo: RegisteredClass,\n ): SmrtCollection<SmrtObject> {\n if (!this.collections.has(classInfo.name)) {\n // `collectionConstructor` is typed optional on RegisteredClass, but the\n // auto-discovery path only reaches here for classes that declare one.\n // Narrow to the non-optional constructor type before instantiating.\n const ctor = classInfo.collectionConstructor as NonNullable<\n RegisteredClass['collectionConstructor']\n >;\n const collection = new ctor({\n ai: this.context.ai,\n db: this.context.db,\n });\n this.collections.set(classInfo.name, collection);\n }\n const collection = this.collections.get(classInfo.name);\n if (!collection) throw new Error(`Collection ${classInfo.name} not found`);\n return collection;\n }\n\n /**\n * Mass-assignment guard (#1540): strip framework/server-managed and\n * `@field({ readonly: true })` fields from a create/update body, and — when an\n * `@smrt({ api: { writable: [...] } })` allowlist is set — intersect with it.\n */\n private applyWritablePolicy(\n objectName: string | undefined,\n data: unknown,\n ): Record<string, unknown> {\n if (!data || typeof data !== 'object') {\n return {};\n }\n\n const serverManaged = new Set([\n 'id',\n 'tenantId',\n 'tenant_id',\n 'createdAt',\n 'created_at',\n 'updatedAt',\n 'updated_at',\n ]);\n\n const readonly = new Set<string>();\n let writable: string[] | null = null;\n\n if (objectName) {\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api;\n if (\n apiConfig &&\n typeof apiConfig === 'object' &&\n Array.isArray((apiConfig as { writable?: unknown }).writable)\n ) {\n writable = (apiConfig as { writable: string[] }).writable;\n }\n\n for (const [name, def] of ObjectRegistry.getFields(objectName)) {\n if (def && (def.readonly === true || def._meta?.readonly === true)) {\n readonly.add(name);\n }\n }\n }\n\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith('_')) continue;\n if (serverManaged.has(key)) continue;\n if (readonly.has(key)) continue;\n if (writable && !writable.includes(key)) continue;\n result[key] = value;\n }\n return result;\n }\n\n /**\n * Serialize a SmrtObject for a network response, excluding sensitive fields\n * (#1540). Falls back to the value unchanged for non-SmrtObject payloads.\n */\n private toPublicData(object: unknown): unknown {\n const serializable = object as { toPublicJSON?: () => unknown } | null;\n return typeof serializable?.toPublicJSON === 'function'\n ? serializable.toPublicJSON()\n : object;\n }\n\n /**\n * Create a JSON read response with conditional-GET support (#1757): a strong\n * body-hash ETag, `If-None-Match` → 304 with an empty body, and the\n * Cache-Control policy resolved from the object's `@smrt({ api })` config\n * (private + revalidatable by default; shared `s-maxage` only for public\n * models that opt in). Tenant-scoped models are always private: their bodies\n * vary with tenant context, which URL-keyed shared caches cannot see.\n */\n private createReadResponse(\n req: Request,\n objectName: string | undefined,\n payload: unknown,\n ): Response {\n const config = objectName\n ? ObjectRegistry.getConfig(objectName)\n : undefined;\n const apiConfig = config?.api;\n // Canonical accessor covers both `@smrt({ tenantScoped })` and the\n // manifest-merged `@TenantScoped()` decorator form; the raw config flag is\n // a belt-and-braces fallback for registrations not yet normalized.\n const tenantScoped = objectName\n ? ObjectRegistry.isTenantScoped(objectName) || !!config?.tenantScoped\n : false;\n if (objectName) {\n warnIfSharedCacheNeutralized(objectName, apiConfig, tenantScoped);\n }\n return conditionalJsonResponse(\n req,\n payload,\n resolveReadCacheControl(apiConfig, { tenantScoped }),\n );\n }\n\n /**\n * Create JSON response with proper headers\n */\n private createJsonResponse(data: unknown, status = 200): Response {\n return new Response(JSON.stringify(data), {\n status,\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n }\n\n /**\n * Create error response\n */\n private createErrorResponse(status: number, message: string): Response {\n return new Response(JSON.stringify({ error: message }), {\n status,\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n }\n\n /**\n * Resolve the allowed `Access-Control-Allow-Origin` for a request (#1540).\n * Returns the request's `Origin` only when it is in the configured allowlist;\n * never `*`. Returns null when CORS should not be applied.\n */\n private resolveAllowedOrigin(req: Request): string | null {\n if (!this.config.enableCors) return null;\n const allowed = this.config.allowedOrigins;\n if (!allowed || allowed.length === 0) return null;\n const origin = req.headers.get('origin');\n return origin && allowed.includes(origin) ? origin : null;\n }\n\n /**\n * Create CORS preflight response\n */\n private createCorsResponse(req: Request): Response {\n const headers: Record<string, string> = {\n 'Access-Control-Allow-Methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type,Authorization',\n 'Access-Control-Max-Age': '86400',\n };\n const origin = this.resolveAllowedOrigin(req);\n if (origin) {\n headers['Access-Control-Allow-Origin'] = origin;\n headers.Vary = 'Origin';\n }\n return new Response(null, { status: 200, headers });\n }\n\n /**\n * Add CORS headers to response\n */\n private addCorsHeaders(response: Response, req: Request): Response {\n const origin = this.resolveAllowedOrigin(req);\n if (!origin) return response;\n\n const headers = new Headers(response.headers);\n headers.set('Access-Control-Allow-Origin', origin);\n headers.set('Vary', 'Origin');\n headers.set(\n 'Access-Control-Allow-Methods',\n 'GET,POST,PUT,PATCH,DELETE,OPTIONS',\n );\n headers.set('Access-Control-Allow-Headers', 'Content-Type,Authorization');\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n }\n\n /**\n * Simple pluralization (basic implementation)\n */\n private pluralize(word: string): string {\n if (word.endsWith('y')) {\n return `${word.slice(0, -1)}ies`;\n }\n if (word.endsWith('s') || word.endsWith('sh') || word.endsWith('ch')) {\n return `${word}es`;\n }\n return `${word}s`;\n }\n}\n\n// REST Server Utilities\n\nexport interface RestServerConfig extends APIConfig {\n healthCheck?: {\n enabled?: boolean;\n path?: string;\n customChecks?: (() => Promise<boolean>)[];\n };\n}\n\n/**\n * Create REST server with health checks using Bun\n */\nexport function createRestServer(\n objects: (typeof SmrtObject)[],\n context: APIContext = {},\n config: RestServerConfig = {},\n): { server: http.Server; url: string } {\n // Register objects if not already registered\n objects.forEach((obj) => {\n if (!ObjectRegistry.hasClass(obj.name)) {\n console.warn(`Object ${obj.name} not registered with @smrt decorator`);\n }\n });\n\n const generator = new APIGenerator(config, context);\n const { server, url } = generator.createServer();\n\n console.log(`🚀 smrt REST API server running at ${url}`);\n\n return { server, url };\n}\n\n/**\n * Start server with graceful shutdown\n */\nexport function startRestServer(\n objects: (typeof SmrtObject)[],\n context: APIContext = {},\n config: RestServerConfig = {},\n): Promise<() => Promise<void>> {\n return new Promise((resolve) => {\n const { server, url } = createRestServer(objects, context, config);\n\n // Graceful shutdown function\n const shutdown = (): Promise<void> => {\n return new Promise((shutdownResolve) => {\n console.log('🛑 Shutting down server gracefully...');\n // Node's http.Server#close stops accepting new connections and invokes\n // the callback once in-flight ones drain. (The prior `.stop()` was a\n // Bun-era API absent on http.Server — it would have thrown.)\n server.close(() => {\n console.log('✅ Server shut down complete');\n shutdownResolve();\n });\n });\n };\n\n // Handle shutdown signals\n process.on('SIGTERM', shutdown);\n process.on('SIGINT', shutdown);\n\n resolve(shutdown);\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"rest.js","names":[],"sources":["../../src/generators/rest.ts"],"sourcesContent":["/**\n * High-performance REST API generator for smrt objects using Node.js HTTP server\n *\n * Designed for minimal bundle size and maximum performance\n */\n\nimport http from 'node:http';\nimport type { SmrtCollection } from '../collection';\nimport {\n isTenantScopedClassResolved,\n resolveDispatchTenantScope,\n} from '../dispatch';\nimport type { SmrtObject } from '../object';\nimport { ObjectRegistry } from '../registry';\nimport type { RegisteredClass, SmrtObjectConstructor } from '../registry/types';\nimport {\n processSyncApplyBatch,\n SYNC_APPLY_ROUTE_SEGMENTS,\n type SyncApplyOp,\n type SyncApplyTarget,\n} from '../sync/apply';\nimport { handleChangesRoute } from './changes-route';\nimport {\n conditionalJsonResponse,\n resolveReadCacheControl,\n warnIfSharedCacheNeutralized,\n warnIfTenantScopedPublicRead,\n} from './conditional-get';\n\nexport interface APIConfig {\n basePath?: string;\n enableCors?: boolean;\n /**\n * Explicit CORS origin allowlist (#1540). Required when `enableCors` is true —\n * the generator never emits `Access-Control-Allow-Origin: *`. A request's\n * `Origin` is echoed only when it appears here.\n */\n allowedOrigins?: string[];\n customRoutes?: Record<string, (req: Request) => Promise<Response>>;\n authMiddleware?: (\n objectName: string,\n action: string,\n ) => (req: Request) => Promise<Request | Response>;\n port?: number;\n hostname?: string;\n}\n\nexport interface APIContext {\n db?: unknown;\n ai?: unknown;\n user?: {\n id: string;\n username?: string;\n roles?: string[];\n };\n}\n\n/**\n * High-performance API generator using native Bun\n */\nexport class APIGenerator {\n private config: APIConfig;\n private collections = new Map<string, SmrtCollection<SmrtObject>>();\n private context: APIContext;\n\n constructor(config: APIConfig = {}, context: APIContext = {}) {\n this.config = {\n basePath: '/api/v1',\n // Security defaults (#1540): bind to loopback and keep CORS off unless an\n // explicit origin allowlist is supplied. No `Access-Control-Allow-Origin: *`.\n enableCors: false,\n port: 3000,\n hostname: '127.0.0.1',\n ...config,\n };\n this.context = context;\n }\n\n /**\n * Register a pre-configured collection instance for API exposure\n *\n * @param name - URL path segment for the collection (e.g., 'products' for /api/products)\n * @param collection - Pre-initialized SmrtCollection instance\n */\n registerCollection(\n name: string,\n collection: SmrtCollection<SmrtObject>,\n ): void {\n this.collections.set(name, collection);\n }\n\n /**\n * Resolve the database for generator-level routes that are not object-scoped\n * (currently the `_changes` feed, #1758).\n *\n * Prefers the explicit `APIContext.db`, but falls back to the first\n * registered collection's live handle so the feed works under the documented\n * `registerCollection()` wiring, where callers configure collections with a\n * database but never populate `APIContext.db`. Without the fallback the feed\n * would 503 even though the database is reachable.\n */\n private resolveContextDb(): unknown {\n return (\n this.context.db ?? this.collections.values().next().value?.db ?? undefined\n );\n }\n\n /**\n * Create Node.js HTTP server with all routes\n */\n createServer(): { server: http.Server; url: string } {\n const server = http.createServer(async (req, res) => {\n try {\n const request = await this.nodeRequestToWebRequest(req);\n const response = await this.handleRequest(request);\n await this.webResponseToNodeResponse(response, res);\n } catch (_error) {\n res.statusCode = 500;\n res.end('Internal Server Error');\n }\n });\n\n server.listen(this.config.port, this.config.hostname);\n\n return {\n server,\n url: `http://${this.config.hostname}:${this.config.port}`,\n };\n }\n\n /**\n * Convert stream to string\n */\n private async streamToString(stream: http.IncomingMessage): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n }\n return Buffer.concat(chunks).toString('utf-8');\n }\n\n /**\n * Convert Node.js IncomingMessage to Web Request\n */\n private async nodeRequestToWebRequest(\n req: http.IncomingMessage,\n ): Promise<Request> {\n const url = `http://${this.config.hostname}:${this.config.port}${req.url}`;\n const method = req.method || 'GET';\n const headers = new Headers();\n\n for (const [key, value] of Object.entries(req.headers)) {\n if (value) {\n headers.set(key, Array.isArray(value) ? value[0] : value);\n }\n }\n\n let body: string | undefined;\n if (method !== 'GET' && method !== 'HEAD') {\n body = await this.streamToString(req);\n }\n\n return new Request(url, {\n method,\n headers,\n body,\n });\n }\n\n /**\n * Convert Web Response to Node.js ServerResponse\n */\n private async webResponseToNodeResponse(\n webResponse: Response,\n res: http.ServerResponse,\n ): Promise<void> {\n res.statusCode = webResponse.status;\n\n // Set headers\n webResponse.headers.forEach((value, key) => {\n res.setHeader(key, value);\n });\n\n // Send body\n if (webResponse.body) {\n const reader = webResponse.body.getReader();\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n res.write(value);\n }\n }\n\n res.end();\n }\n\n /**\n * Generate fetch handler function (for serverless environments)\n */\n generateHandler(): (req: Request) => Promise<Response> {\n return (req) => this.handleRequest(req);\n }\n\n /**\n * Main request handler using native Bun APIs\n */\n private async handleRequest(req: Request): Promise<Response> {\n const url = new URL(req.url);\n\n // Handle CORS preflight\n if (req.method === 'OPTIONS' && this.config.enableCors) {\n return this.createCorsResponse(req);\n }\n\n // Handle custom routes first\n if (this.config.customRoutes) {\n for (const [path, handler] of Object.entries(this.config.customRoutes)) {\n if (url.pathname === `${this.config.basePath}${path}`) {\n const response = await handler(req);\n return this.addCorsHeaders(response, req);\n }\n }\n }\n\n // Change-feed route (#1758): auth-guarded, tenant-scoped cursor read\n // over the _smrt_changes log. Logic lives in ./changes-route.ts.\n if (url.pathname === `${this.config.basePath}/_changes`) {\n const response = await handleChangesRoute(req, {\n authMiddleware: this.config.authMiddleware,\n db: this.resolveContextDb(),\n });\n return this.addCorsHeaders(response, req);\n }\n\n // Handle object routes\n if (url.pathname.startsWith(this.config.basePath || '')) {\n const response = await this.handleObjectRoute(req, url);\n return this.addCorsHeaders(response, req);\n }\n\n // Not found\n return this.createErrorResponse(404, 'Not found');\n }\n\n /**\n * Handle CRUD routes for SMRT objects\n */\n private async handleObjectRoute(req: Request, url: URL): Promise<Response> {\n const pathParts = url.pathname\n .replace(this.config.basePath || '', '')\n .split('/')\n .filter(Boolean);\n\n if (pathParts.length === 0) {\n return this.createErrorResponse(400, 'Object type required');\n }\n\n // Reserved batch write contract route (#1759): POST {basePath}/sync/apply.\n if (\n pathParts.length === SYNC_APPLY_ROUTE_SEGMENTS.length &&\n SYNC_APPLY_ROUTE_SEGMENTS.every((segment, i) => pathParts[i] === segment)\n ) {\n if (req.method !== 'POST') {\n return this.createErrorResponse(405, 'Method not allowed');\n }\n return await this.handleSyncApply(req);\n }\n\n const objectType = pathParts[0];\n const objectId = pathParts[1];\n\n // Check for explicitly registered collection first\n if (this.collections.has(objectType)) {\n const collection = this.collections.get(objectType);\n if (!collection) throw new Error(`Collection ${objectType} not found`);\n\n const objectName = this.getCollectionObjectName(collection) || objectType;\n\n // Apply auth middleware if configured\n if (this.config.authMiddleware) {\n const authCheck = this.config.authMiddleware(\n objectName,\n req.method.toLowerCase(),\n );\n const authResult = await authCheck(req);\n if (authResult instanceof Response) {\n return authResult; // Auth failed\n }\n // Auth passed, use the potentially modified request\n req = authResult;\n } else if (!this.isRoutePublic(objectName, req.method)) {\n // Fail-closed (#1540): no auth middleware wired and the object isn't\n // marked `@smrt({ api: { public } })` → refuse rather than serve open.\n return this.createErrorResponse(401, 'Authentication required');\n }\n\n // Use registered collection directly\n return await this.executeCrudOperation(\n req,\n collection,\n objectId,\n url,\n objectName,\n );\n }\n\n // Fall back to auto-discovery via ObjectRegistry, matching the URL segment\n // against the canonical manifest `collection` value (#1794).\n const classInfo = this.findClassByCollectionSegment(objectType);\n\n if (!classInfo) {\n return this.createErrorResponse(\n 404,\n `Object type '${objectType}' not found`,\n );\n }\n\n // Apply auth middleware if configured\n if (this.config.authMiddleware) {\n const authCheck = this.config.authMiddleware(\n classInfo.name,\n req.method.toLowerCase(),\n );\n const authResult = await authCheck(req);\n if (authResult instanceof Response) {\n return authResult; // Auth failed\n }\n // Auth passed, use the potentially modified request\n req = authResult;\n } else if (!this.isRoutePublic(classInfo.name, req.method)) {\n // Fail-closed (#1540): see registered-collection branch above.\n return this.createErrorResponse(401, 'Authentication required');\n }\n\n // Get or create collection\n const collection = await this.getCollection(classInfo);\n\n return await this.executeCrudOperation(\n req,\n collection,\n objectId,\n url,\n classInfo.name,\n );\n }\n\n /**\n * Execute CRUD operation on a collection\n */\n private async executeCrudOperation(\n req: Request,\n collection: SmrtCollection<SmrtObject>,\n objectId: string | undefined,\n url: URL,\n objectName?: string,\n ): Promise<Response> {\n try {\n const action = this.getCrudAction(req.method, objectId);\n if (action && !this.isApiActionEnabled(objectName, action)) {\n return this.createErrorResponse(405, 'Method not allowed');\n }\n\n // Handle special /count endpoint\n if (objectId === 'count' && req.method === 'GET') {\n return await this.handleCount(collection, url.searchParams, objectName);\n }\n\n // Route to appropriate CRUD operation\n switch (req.method) {\n case 'GET':\n return objectId\n ? await this.handleGet(collection, objectId, req, objectName)\n : await this.handleList(\n collection,\n url.searchParams,\n req,\n objectName,\n );\n\n case 'POST':\n return await this.handleCreate(collection, req, objectName);\n\n case 'PUT':\n case 'PATCH':\n if (!objectId) {\n return this.createErrorResponse(\n 400,\n 'Object ID required for update',\n );\n }\n return await this.handleUpdate(collection, objectId, req, objectName);\n\n case 'DELETE':\n if (!objectId) {\n return this.createErrorResponse(\n 400,\n 'Object ID required for delete',\n );\n }\n return await this.handleDelete(collection, objectId);\n\n default:\n return this.createErrorResponse(405, 'Method not allowed');\n }\n } catch (error) {\n console.error('API Error:', error);\n return this.createErrorResponse(500, 'Internal server error');\n }\n }\n\n private getCrudAction(\n method: string,\n objectId: string | undefined,\n ): 'list' | 'get' | 'create' | 'update' | 'delete' | null {\n switch (method) {\n case 'GET':\n return objectId && objectId !== 'count' ? 'get' : 'list';\n case 'POST':\n return 'create';\n case 'PUT':\n case 'PATCH':\n return 'update';\n case 'DELETE':\n return 'delete';\n default:\n return null;\n }\n }\n\n /**\n * Fail-closed authorization posture (#1540). Returns true only when the\n * object opts out of auth via `@smrt({ api: { public } })`:\n * - `public: true` → all methods are public.\n * - `public: 'read'` → only safe (GET) methods are public.\n * Everything else requires an `authMiddleware` to be configured.\n */\n private isRoutePublic(\n objectName: string | undefined,\n method: string,\n ): boolean {\n if (!objectName) return false;\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api;\n if (!apiConfig || typeof apiConfig !== 'object') return false;\n const publicAccess = (apiConfig as { public?: boolean | 'read' }).public;\n if (publicAccess === true) return true;\n if (publicAccess === 'read') return method.toUpperCase() === 'GET';\n return false;\n }\n\n private isApiActionEnabled(\n objectName: string | undefined,\n action: 'list' | 'get' | 'create' | 'update' | 'delete',\n ): boolean {\n if (!objectName) {\n return true;\n }\n\n const config = ObjectRegistry.getConfig(objectName);\n const apiConfig = config.api;\n\n if (apiConfig === false) {\n return false;\n }\n\n if (apiConfig && typeof apiConfig === 'object') {\n if (apiConfig.include && !apiConfig.include.includes(action)) {\n return false;\n }\n\n if (apiConfig.exclude?.includes(action)) {\n return false;\n }\n }\n\n return true;\n }\n\n private getCollectionObjectName(\n collection: SmrtCollection<SmrtObject>,\n ): string | undefined {\n // `_itemClass` lives on the instance (protected getter) or the collection\n // constructor (static). Read it through a structural shape rather than the\n // class's private surface; behavior is unchanged.\n const itemClass: SmrtObjectConstructor | undefined =\n (collection as unknown as { _itemClass?: SmrtObjectConstructor })\n ._itemClass ||\n (\n collection.constructor as unknown as {\n _itemClass?: SmrtObjectConstructor;\n }\n )?._itemClass;\n\n if (!itemClass) {\n return undefined;\n }\n\n const registered = ObjectRegistry.getClassByConstructor(itemClass);\n return registered?.qualifiedName || registered?.name || itemClass.name;\n }\n\n /**\n * Fail-closed tenant read scope (#1782).\n *\n * A `@TenantScoped` model served over a public/anonymous read has no ambient\n * tenant context, so the tenancy interceptor (optional mode) passes the query\n * through UNFILTERED and returns every tenant's rows. When tenancy is enabled\n * but no tenant is active, this returns a `{ tenantId: null }` filter so reads\n * fail closed to NULL-tenant (global) rows only — mirroring the dispatch\n * resolver's \"enforced, no active tenant → global rows only\" convention\n * (`resolveDispatchTenantScope` is the core-level trust anchor tenancy fills\n * in; core cannot import tenancy directly). Returns undefined when a tenant IS\n * active (the interceptor filters by it) or tenancy is disabled (no isolation\n * to enforce).\n *\n * Tenant scoping is recognized across BOTH registration forms (#1782): the\n * `@smrt({ tenantScoped })` config / manifest-merged form via\n * `ObjectRegistry.isTenantScoped`, and the standalone `@TenantScoped()`\n * decorator via the tenancy-filled `isTenantScopedClassResolved` hook — so a\n * `@TenantScoped()`-only public model can't slip past the guard regardless of\n * manifest timing.\n */\n private resolveTenantReadScope(\n objectName?: string,\n ): { tenantId: null } | undefined {\n if (\n !objectName ||\n !(\n ObjectRegistry.isTenantScoped(objectName) ||\n !!ObjectRegistry.getConfig(objectName)?.tenantScoped ||\n isTenantScopedClassResolved(objectName)\n )\n ) {\n return undefined;\n }\n const scope = resolveDispatchTenantScope();\n return scope.enforced && scope.tenantId === null\n ? { tenantId: null }\n : undefined;\n }\n\n /**\n * Merge the fail-closed tenant read scope (#1782) into a query's WHERE clause.\n * When the scope is active (global-only), any client-supplied tenant filter is\n * dropped first so a `?tenantId=...` / `?tenant_id[ne]=...` query param can\n * never widen the scope, then NULL-tenant is forced. Returns the original\n * `where` untouched when the scope is inactive.\n */\n private applyTenantReadScope(\n objectName: string | undefined,\n where: Record<string, string | string[]> | undefined,\n ): Record<string, unknown> | undefined {\n const scope = this.resolveTenantReadScope(objectName);\n if (!scope) {\n return where;\n }\n const cleaned: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(where ?? {})) {\n // A key may carry an operator suffix, e.g. \"tenant_id !=\" — match on the\n // field portion so no operator variant slips a client tenant filter past.\n const field = key.split(/\\s+/)[0];\n if (field === 'tenantId' || field === 'tenant_id') {\n continue;\n }\n cleaned[key] = value;\n }\n cleaned.tenantId = null;\n return cleaned;\n }\n\n /**\n * Handle GET /objects/:id\n */\n private async handleGet(\n collection: SmrtCollection<SmrtObject>,\n id: string,\n req: Request,\n objectName?: string,\n ): Promise<Response> {\n // #1782: scope a public/anonymous read of a tenant-scoped model to global\n // (NULL-tenant) rows when no tenant context is active.\n const scope = this.resolveTenantReadScope(objectName);\n const object = scope\n ? await collection.get({ id, ...scope })\n : await collection.get(id);\n if (!object) {\n return this.createErrorResponse(404, 'Object not found');\n }\n return this.createReadResponse(req, objectName, this.toPublicData(object));\n }\n\n /**\n * Handle GET /objects (list with query params)\n */\n private async handleList(\n collection: SmrtCollection<SmrtObject>,\n params: URLSearchParams,\n req: Request,\n objectName?: string,\n ): Promise<Response> {\n const limit = Number.parseInt(params.get('limit') || '50', 10);\n const offset = Number.parseInt(params.get('offset') || '0', 10);\n const orderBy = params.get('orderBy') || 'created_at DESC';\n\n // Build where clause from query params\n // Convert REST-style operators (price[gt]) to SQL-style (price >)\n const where: Record<string, string | string[]> = {};\n for (const [key, value] of params.entries()) {\n if (!['limit', 'offset', 'orderBy'].includes(key)) {\n // Parse REST operator format: field[operator]\n const match = key.match(/^(.+)\\[(.+)\\]$/);\n if (match) {\n const field = match[1];\n const operator = match[2];\n // Map REST operators to SQL operators\n const operatorMap: Record<string, string> = {\n gt: '>',\n gte: '>=',\n lt: '<',\n lte: '<=',\n ne: '!=',\n in: 'in',\n like: 'like',\n };\n const sqlOperator = operatorMap[operator] || operator;\n const sqlKey = `${field} ${sqlOperator}`;\n // Handle 'in' operator - convert comma-separated string to array\n where[sqlKey] = operator === 'in' ? value.split(',') : value;\n } else {\n where[key] = value;\n }\n }\n }\n\n // #1782: fail closed to global (NULL-tenant) rows for a public/anonymous\n // read of a tenant-scoped model with no active tenant context.\n const scopedWhere = this.applyTenantReadScope(objectName, where);\n\n const objects = await collection.list({\n where:\n scopedWhere && Object.keys(scopedWhere).length > 0\n ? scopedWhere\n : undefined,\n limit,\n offset,\n orderBy,\n });\n\n return this.createReadResponse(\n req,\n objectName,\n objects.map((object: SmrtObject) => this.toPublicData(object)),\n );\n }\n\n /**\n * Handle GET /objects/count\n */\n private async handleCount(\n collection: SmrtCollection<SmrtObject>,\n params: URLSearchParams,\n objectName?: string,\n ): Promise<Response> {\n // Build where clause from query params (same logic as handleList)\n const where: Record<string, string | string[]> = {};\n for (const [key, value] of params.entries()) {\n // Parse REST operator format: field[operator]\n const match = key.match(/^(.+)\\[(.+)\\]$/);\n if (match) {\n const field = match[1];\n const operator = match[2];\n // Map REST operators to SQL operators\n const operatorMap: Record<string, string> = {\n gt: '>',\n gte: '>=',\n lt: '<',\n lte: '<=',\n ne: '!=',\n in: 'in',\n like: 'like',\n };\n const sqlOperator = operatorMap[operator] || operator;\n const sqlKey = `${field} ${sqlOperator}`;\n // Handle 'in' operator - convert comma-separated string to array\n where[sqlKey] = operator === 'in' ? value.split(',') : value;\n } else {\n where[key] = value;\n }\n }\n\n // #1782: a count leaks cross-tenant cardinality just as a list leaks rows,\n // so apply the same fail-closed global scope for anonymous tenant reads.\n const scopedWhere = this.applyTenantReadScope(objectName, where);\n\n const count = await collection.count({\n where:\n scopedWhere && Object.keys(scopedWhere).length > 0\n ? scopedWhere\n : undefined,\n });\n\n return this.createJsonResponse({ count });\n }\n\n /**\n * Handle POST /objects\n */\n private async handleCreate(\n collection: SmrtCollection<SmrtObject>,\n req: Request,\n objectName?: string,\n ): Promise<Response> {\n const data = this.applyWritablePolicy(objectName, await req.json());\n const object = await collection.create({ ...data, _skipLoad: true });\n await object.save();\n return this.createJsonResponse(this.toPublicData(object), 201);\n }\n\n /**\n * Handle PUT/PATCH /objects/:id\n */\n private async handleUpdate(\n collection: SmrtCollection<SmrtObject>,\n id: string,\n req: Request,\n objectName?: string,\n ): Promise<Response> {\n const data = this.applyWritablePolicy(objectName, await req.json());\n const object = await collection.get(id);\n\n if (!object) {\n return this.createErrorResponse(404, 'Object not found');\n }\n\n // Update object properties\n Object.assign(object, data);\n await object.save();\n\n return this.createJsonResponse(this.toPublicData(object));\n }\n\n /**\n * Handle DELETE /objects/:id\n */\n private async handleDelete(\n collection: SmrtCollection<SmrtObject>,\n id: string,\n ): Promise<Response> {\n const object = await collection.get(id);\n\n if (!object) {\n return this.createErrorResponse(404, 'Object not found');\n }\n\n await object.delete();\n return new Response(null, { status: 204 });\n }\n\n /**\n * Handle POST /sync/apply — the idempotent batch write contract (#1759).\n * All processing lives in `sync/apply.ts`; this method only adapts the\n * generator's existing collection resolution, auth, action gating, and\n * writable policy into a {@link SyncApplyTarget} per item.\n */\n private async handleSyncApply(req: Request): Promise<Response> {\n // Buffer the body once. Request bodies are single-read streams, and the\n // per-item auth middleware below must receive a readable body exactly as\n // it does on CRUD routes (where auth runs before parsing) — so each\n // middleware invocation gets a fresh Request rebuilt from this buffer.\n let rawBody: string;\n let body: unknown;\n try {\n rawBody = await req.text();\n body = JSON.parse(rawBody);\n } catch {\n return this.createErrorResponse(400, 'Invalid JSON body');\n }\n\n const outcome = await processSyncApplyBatch(body, {\n resolveTarget: (objectSegment) =>\n this.resolveSyncApplyTarget(objectSegment, req, rawBody),\n });\n return this.createJsonResponse(outcome.body, outcome.status);\n }\n\n /**\n * Resolve a sync item's `object` segment exactly like `handleObjectRoute`\n * resolves a CRUD URL segment (registered collections first, then registry\n * auto-discovery), and wrap the generator's per-object machinery.\n */\n private async resolveSyncApplyTarget(\n objectSegment: string,\n req: Request,\n rawBody: string,\n ): Promise<SyncApplyTarget | null> {\n let collection: SmrtCollection<SmrtObject> | null = null;\n let objectName: string | null = null;\n\n const registered = this.collections.get(objectSegment);\n if (registered) {\n collection = registered;\n objectName = this.getCollectionObjectName(registered) || objectSegment;\n } else {\n // Same canonical-`collection` match as the CRUD path so a client and the\n // batch contract resolve identical segments (#1794).\n const info = this.findClassByCollectionSegment(objectSegment);\n if (info) {\n collection = await this.getCollection(info);\n objectName = info.name;\n }\n }\n\n if (!collection || !objectName) {\n return null;\n }\n\n const resolvedName = objectName;\n const resolvedCollection = collection;\n return {\n objectName: resolvedName,\n collection: resolvedCollection,\n isOpAllowed: (op: SyncApplyOp) =>\n this.isApiActionEnabled(resolvedName, op),\n authorize: async (op: SyncApplyOp) => {\n if (this.config.authMiddleware) {\n const verb =\n op === 'create' ? 'post' : op === 'update' ? 'put' : 'delete';\n const authCheck = this.config.authMiddleware(resolvedName, verb);\n // Fresh Request per invocation: the batch handler already consumed\n // the original body, and middleware may read it (body-based auth,\n // request signing) exactly as it can on CRUD routes.\n const authResult = await authCheck(\n new Request(req.url, {\n method: req.method,\n headers: req.headers,\n body: rawBody,\n }),\n );\n if (authResult instanceof Response) {\n return authResult.status === 401 ? 'auth_required' : 'forbidden';\n }\n return 'ok';\n }\n // Fail-closed (#1540): every sync op is mutating, so only\n // `@smrt({ api: { public: true } })` objects apply without auth.\n return this.isRoutePublic(resolvedName, 'POST')\n ? 'ok'\n : 'auth_required';\n },\n prepare: (payload) => this.applyWritablePolicy(resolvedName, payload),\n };\n }\n\n /**\n * Get or create collection instance\n */\n private async getCollection(\n classInfo: RegisteredClass,\n ): Promise<SmrtCollection<SmrtObject>> {\n if (!this.collections.has(classInfo.name)) {\n // `collectionConstructor` is typed optional on RegisteredClass, but the\n // auto-discovery path only reaches here for classes that declare one.\n // Narrow to the non-optional constructor type before instantiating.\n const ctor = classInfo.collectionConstructor as NonNullable<\n RegisteredClass['collectionConstructor']\n >;\n const collection = new ctor({\n ai: this.context.ai,\n db: this.context.db,\n });\n // Await initialize() so the auto-discovered collection is actually usable\n // (its DB is wired). Without this, list()/get() throw \"Database accessed\n // before initialization\" — a latent bug that was masked while the\n // generated client's plural URLs never matched the singular-only\n // auto-discovery segment (#1794). The explicit-registerCollection() path\n // receives an already-initialized collection from the caller, so this\n // only affects the auto-discovery factory.\n await collection.initialize();\n this.collections.set(classInfo.name, collection);\n }\n const collection = this.collections.get(classInfo.name);\n if (!collection) throw new Error(`Collection ${classInfo.name} not found`);\n return collection;\n }\n\n /**\n * Mass-assignment guard (#1540): strip framework/server-managed and\n * `@field({ readonly: true })` fields from a create/update body, and — when an\n * `@smrt({ api: { writable: [...] } })` allowlist is set — intersect with it.\n */\n private applyWritablePolicy(\n objectName: string | undefined,\n data: unknown,\n ): Record<string, unknown> {\n if (!data || typeof data !== 'object') {\n return {};\n }\n\n const serverManaged = new Set([\n 'id',\n 'tenantId',\n 'tenant_id',\n 'createdAt',\n 'created_at',\n 'updatedAt',\n 'updated_at',\n ]);\n\n const readonly = new Set<string>();\n let writable: string[] | null = null;\n\n if (objectName) {\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api;\n if (\n apiConfig &&\n typeof apiConfig === 'object' &&\n Array.isArray((apiConfig as { writable?: unknown }).writable)\n ) {\n writable = (apiConfig as { writable: string[] }).writable;\n }\n\n for (const [name, def] of ObjectRegistry.getFields(objectName)) {\n if (def && (def.readonly === true || def._meta?.readonly === true)) {\n readonly.add(name);\n }\n }\n }\n\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith('_')) continue;\n if (serverManaged.has(key)) continue;\n if (readonly.has(key)) continue;\n if (writable && !writable.includes(key)) continue;\n result[key] = value;\n }\n return result;\n }\n\n /**\n * Serialize a SmrtObject for a network response, excluding sensitive fields\n * (#1540). Falls back to the value unchanged for non-SmrtObject payloads.\n */\n private toPublicData(object: unknown): unknown {\n const serializable = object as { toPublicJSON?: () => unknown } | null;\n return typeof serializable?.toPublicJSON === 'function'\n ? serializable.toPublicJSON()\n : object;\n }\n\n /**\n * Create a JSON read response with conditional-GET support (#1757): a strong\n * body-hash ETag, `If-None-Match` → 304 with an empty body, and the\n * Cache-Control policy resolved from the object's `@smrt({ api })` config\n * (private + revalidatable by default; shared `s-maxage` only for public\n * models that opt in). Tenant-scoped models are always private: their bodies\n * vary with tenant context, which URL-keyed shared caches cannot see.\n */\n private createReadResponse(\n req: Request,\n objectName: string | undefined,\n payload: unknown,\n ): Response {\n const config = objectName\n ? ObjectRegistry.getConfig(objectName)\n : undefined;\n const apiConfig = config?.api;\n // Canonical accessor covers both `@smrt({ tenantScoped })` and the\n // manifest-merged `@TenantScoped()` decorator form; the raw config flag is\n // a belt-and-braces fallback for registrations not yet normalized.\n const tenantScoped = objectName\n ? ObjectRegistry.isTenantScoped(objectName) || !!config?.tenantScoped\n : false;\n if (objectName) {\n warnIfSharedCacheNeutralized(objectName, apiConfig, tenantScoped);\n warnIfTenantScopedPublicRead(objectName, apiConfig, tenantScoped);\n }\n return conditionalJsonResponse(\n req,\n payload,\n resolveReadCacheControl(apiConfig, { tenantScoped }),\n );\n }\n\n /**\n * Create JSON response with proper headers\n */\n private createJsonResponse(data: unknown, status = 200): Response {\n return new Response(JSON.stringify(data), {\n status,\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n }\n\n /**\n * Create error response\n */\n private createErrorResponse(status: number, message: string): Response {\n return new Response(JSON.stringify({ error: message }), {\n status,\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n }\n\n /**\n * Resolve the allowed `Access-Control-Allow-Origin` for a request (#1540).\n * Returns the request's `Origin` only when it is in the configured allowlist;\n * never `*`. Returns null when CORS should not be applied.\n */\n private resolveAllowedOrigin(req: Request): string | null {\n if (!this.config.enableCors) return null;\n const allowed = this.config.allowedOrigins;\n if (!allowed || allowed.length === 0) return null;\n const origin = req.headers.get('origin');\n return origin && allowed.includes(origin) ? origin : null;\n }\n\n /**\n * Create CORS preflight response\n */\n private createCorsResponse(req: Request): Response {\n const headers: Record<string, string> = {\n 'Access-Control-Allow-Methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type,Authorization',\n 'Access-Control-Max-Age': '86400',\n };\n const origin = this.resolveAllowedOrigin(req);\n if (origin) {\n headers['Access-Control-Allow-Origin'] = origin;\n headers.Vary = 'Origin';\n }\n return new Response(null, { status: 200, headers });\n }\n\n /**\n * Add CORS headers to response\n */\n private addCorsHeaders(response: Response, req: Request): Response {\n const origin = this.resolveAllowedOrigin(req);\n if (!origin) return response;\n\n const headers = new Headers(response.headers);\n headers.set('Access-Control-Allow-Origin', origin);\n headers.set('Vary', 'Origin');\n headers.set(\n 'Access-Control-Allow-Methods',\n 'GET,POST,PUT,PATCH,DELETE,OPTIONS',\n );\n headers.set('Access-Control-Allow-Headers', 'Content-Type,Authorization');\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n }\n\n /**\n * Resolve a URL/collection segment to a registered class via auto-discovery.\n *\n * The segment IS the canonical `collection` value from the manifest\n * (`RegisteredClass.collection`, e.g. `products` for `Product`) — the same\n * value the generated client, the SvelteKit route generator, and the registry\n * all derive. Match it VERBATIM; do not re-pluralize. The previous\n * implementation pluralized both the URL segment and the class name and\n * compared them, turning an already-plural client URL (`/products`) into\n * `productses`, so the generated client 404'd against auto-discovered routes\n * (#1794). Shared by the CRUD path and the `sync/apply` batch path so they can\n * never drift.\n *\n * @param segment - The URL/collection path segment (e.g. `products`).\n * @returns The matching registered class, or null if none matches.\n */\n private findClassByCollectionSegment(\n segment: string,\n ): RegisteredClass | null {\n for (const [key, info] of ObjectRegistry.getAllClasses()) {\n // Primary match: the canonical manifest `collection` segment.\n if (info.collection === segment) {\n return info;\n }\n // Back-compat fallback for classes registered without a `collection`\n // (inline/test stubs): derive the plural segment from the simple name and\n // compare to the raw URL segment. Issue #951: use the simple name\n // (info.name), not the qualified map key.\n if (\n !info.collection &&\n this.pluralize((info.name || key).toLowerCase()) === segment\n ) {\n return info;\n }\n }\n return null;\n }\n\n /**\n * Simple pluralization (basic implementation)\n */\n private pluralize(word: string): string {\n if (word.endsWith('y')) {\n return `${word.slice(0, -1)}ies`;\n }\n if (word.endsWith('s') || word.endsWith('sh') || word.endsWith('ch')) {\n return `${word}es`;\n }\n return `${word}s`;\n }\n}\n\n// REST Server Utilities\n\nexport interface RestServerConfig extends APIConfig {\n healthCheck?: {\n enabled?: boolean;\n path?: string;\n customChecks?: (() => Promise<boolean>)[];\n };\n}\n\n/**\n * Create REST server with health checks using Bun\n */\nexport function createRestServer(\n objects: (typeof SmrtObject)[],\n context: APIContext = {},\n config: RestServerConfig = {},\n): { server: http.Server; url: string } {\n // Register objects if not already registered\n objects.forEach((obj) => {\n if (!ObjectRegistry.hasClass(obj.name)) {\n console.warn(`Object ${obj.name} not registered with @smrt decorator`);\n }\n });\n\n const generator = new APIGenerator(config, context);\n const { server, url } = generator.createServer();\n\n console.log(`🚀 smrt REST API server running at ${url}`);\n\n return { server, url };\n}\n\n/**\n * Start server with graceful shutdown\n */\nexport function startRestServer(\n objects: (typeof SmrtObject)[],\n context: APIContext = {},\n config: RestServerConfig = {},\n): Promise<() => Promise<void>> {\n return new Promise((resolve) => {\n const { server, url } = createRestServer(objects, context, config);\n\n // Graceful shutdown function\n const shutdown = (): Promise<void> => {\n return new Promise((shutdownResolve) => {\n console.log('🛑 Shutting down server gracefully...');\n // Node's http.Server#close stops accepting new connections and invokes\n // the callback once in-flight ones drain. (The prior `.stop()` was a\n // Bun-era API absent on http.Server — it would have thrown.)\n server.close(() => {\n console.log('✅ Server shut down complete');\n shutdownResolve();\n });\n });\n };\n\n // Handle shutdown signals\n process.on('SIGTERM', shutdown);\n process.on('SIGINT', shutdown);\n\n resolve(shutdown);\n });\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  * - MCPGenerator: Create MCP servers for AI model integration
17
17
  */
18
18
  export * from './adapters/index';
19
+ export { type AppendChangeInput, appendChange, bumpChangeFeed, CHANGE_FEED_INTERCEPTOR_NAME, CHANGE_FEED_TABLE, type ChangeFeedEntry, type ChangeFeedPage, type ChangeFeedRetention, type ChangeOperation, DEFAULT_CHANGES_LIMIT, ensureChangeFeedTable, type GetChangesOptions, getChangesSince, getTenantScopedChangesSince, MAX_CHANGES_LIMIT, pruneChangeFeed, registerChangeFeedWriter, resetChangeFeedWarnings, unregisterChangeFeedWriter, } from './change-feed';
19
20
  export { applyOneToManyChildAccessors, childAccessorName, } from './child-accessors';
20
21
  export * from './class';
21
22
  export * from './collection';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}