@stacksjs/orm 0.70.293 → 0.70.295

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.
@@ -137,14 +137,52 @@ declare function safeJSONOrEmpty(_s: string): unknown;
137
137
  * read path) now gets its casts instead of leaking raw SQLite `"1"`s.
138
138
  */
139
139
  export declare function applyCasts(record: Record<string, any> | null | undefined, casts: Record<string, string | { get: (v: unknown) => unknown, set: (v: unknown) => unknown }> | null | undefined, direction: 'get' | 'set'): any;
140
+ /**
141
+ * A route path with every parameter name flattened to `{}`.
142
+ *
143
+ * The "user routes win" guard compared paths literally, so an app's own
144
+ * `/api/sites/{siteId}` did not suppress the ORM's `/api/sites/{id}` — the two
145
+ * strings differ, so BOTH were registered and the ORM copy carried none of the
146
+ * app's authorization. The app had declared the endpoint and still got a second,
147
+ * unguarded one it never wrote (stacksjs/stacks#2224).
148
+ *
149
+ * The parameter's NAME is the app's business. The shape is what decides whether
150
+ * this URL is already claimed.
151
+ */
152
+ export declare function routeShape(path: string): string;
140
153
  /**
141
154
  * Resolve middleware lists for a model's `useApi` trait value (which may be
142
155
  * `true` or `{ uri, routes, middleware }`).
143
156
  *
144
- * Secure-by-default: mutating routes (store/update/destroy) get `auth`
145
- * unless the model explicitly declares `useApi.middleware` — an explicit
146
- * `middleware: []` is a deliberate opt-out and is honored (with a startup
147
- * warning at the call site). Read routes stay public unless declared.
157
+ * Secure-by-default on BOTH sides: with no declared `useApi.middleware`, read
158
+ * and mutating routes alike get `auth`.
159
+ *
160
+ * #1949 gave the mutating routes that default and deliberately left reads
161
+ * public, reasoning that catalog tables (products, posts) want anonymous
162
+ * browsing. The cost of that default landed on models that are not catalogs: a
163
+ * model opting into the trait without declaring middleware published
164
+ * `GET /api/{uri}` and `GET /api/{uri}/{id}` to anyone. In one real app that
165
+ * was `GET /api/users` returning the full customer list — only `password` was
166
+ * `hidden`, so names and emails came back — and the app's own security tests
167
+ * could not see it, because the route was never declared in its route files
168
+ * (stacksjs/stacks#2224).
169
+ *
170
+ * A wrong "public" default is a data breach; a wrong "private" default is a 401
171
+ * on the first request in development. Only one of those is recoverable, so the
172
+ * default is now `auth` and a public read is something an app asks for.
173
+ *
174
+ * Three declaration shapes, so asking is always possible:
175
+ *
176
+ * `middleware: ['auth']` both sides get the list (unchanged)
177
+ * `middleware: []` both sides public — deliberate opt-out,
178
+ * warned about at the call site
179
+ * `middleware: { read, write }` per-side lists
180
+ *
181
+ * The split form exists because the secure default would otherwise make the
182
+ * most common real shape — public catalog reads, authenticated writes —
183
+ * inexpressible: a flat `middleware: []` is the only way to open reads, and it
184
+ * opens writes at the same time. That is a worse trade than the bug being fixed,
185
+ * so `{ read: [], write: ['auth'] }` says it exactly.
148
186
  */
149
187
  export declare function resolveApiMiddleware(useApi: unknown): { read: string[], write: string[], declared: boolean };
150
188
  /**
@@ -49,7 +49,13 @@ declare interface StacksModelDefinition extends Omit<BQBModelDefinition, 'attrib
49
49
  table: string
50
50
  primaryKey?: string
51
51
  autoIncrement?: boolean
52
- traits?: NonNullable<BQBModelDefinition['traits']> & Record<string, unknown>
52
+ traits?: Omit<NonNullable<BQBModelDefinition['traits']>, 'useApi'> & {
53
+ useApi?: boolean | {
54
+ readonly uri?: string
55
+ readonly routes?: readonly string[]
56
+ readonly middleware?: any
57
+ }
58
+ } & Record<string, unknown>
53
59
  indexes?: Array<{ name: string, columns: string[], unique?: boolean, where?: string }>
54
60
  casts?: Record<string, CastType | CasterInterface>
55
61
  attributes: {