@stacksjs/defaults 0.74.29 → 0.74.31

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.
@@ -52,6 +52,28 @@ auth/src/
52
52
  - `Auth.login(credentials: AuthCredentials, options?: TokenCreateOptions): Promise<{ user, token } | null>` — login and create token
53
53
  - `Auth.loginUsingId(userId: number, options?: TokenCreateOptions): Promise<{ user, token } | null>` — login by user ID
54
54
  - `Auth.logout(): Promise<void>` — revoke current token
55
+
56
+ ### Personal access tokens (Sanctum-shaped)
57
+
58
+ `oauth_access_tokens` is polymorphic: `tokenable_type` holds the owner's TABLE
59
+ name (`users`, `authors`) and `tokenable_id` its id there, so any model
60
+ declaring `useAuth` can hold tokens - not only `User`.
61
+
62
+ - `createToken(id, name, scopes, { tokenableType })` — mint one. Returns the
63
+ plaintext ONCE (`plainTextToken`); the table stores a hash and nothing can
64
+ recover it afterwards. `tokenableType` defaults to `users`.
65
+ - `tokens(id, tokenableType?)` — list an owner's live tokens.
66
+ - `tokenCan(scope)` / `tokenCanAll` / `tokenCanAny` / `tokenAbilities` — check
67
+ the current request's token.
68
+ - `revokeToken`, `revokeTokenById`, `revokeAllTokens(id, type?)`,
69
+ `revokeOtherTokens(id, type?)` — revocation also revokes the paired refresh
70
+ token, which a raw row delete does not.
71
+ - `setTrailActor(id)` — attribute writes in a queue job or CLI run that has no
72
+ request to read a user from.
73
+
74
+ The `PersonalAccessToken` model maps the same table, so `owner.with('tokenable')`
75
+ lists exactly what `createToken` minted. It deliberately generates no CRUD
76
+ routes: minting and revoking both carry semantics a generic route does not.
55
77
  - `Auth.once(credentials: AuthCredentials): Promise<boolean>` — one-time auth without token
56
78
  - `Auth.requestToken(credentials, clientId, clientSecret): Promise<{ token } | null>` — OAuth token request
57
79
 
@@ -66,7 +66,7 @@ globalThis.toggleDark = toggleDark
66
66
  - **Custom Functions**: From `resources/functions/` (counter, dark mode, GPX, geo utilities)
67
67
 
68
68
  ### Server Auto-Imports (100+)
69
- - **All ORM Models**: User, Post, Author, Product, Order, Payment, Customer, etc. (97 models)
69
+ - **All ORM Models**: User, Post, Author, Product, Order, Payment, Customer, etc. (98 models)
70
70
  - **Request Models**: UserRequest, PostRequest, OrderRequest, etc.
71
71
  - **Actions**: Action types and helpers
72
72
  - **Schema**: validation schema builder
@@ -98,10 +98,12 @@ generated model types stay precise.
98
98
  | `useUuid` | UUID column alongside the primary key |
99
99
  | `useTimestamps` (alias `timestampable`) | `created_at` / `updated_at`. On by default |
100
100
  | `useSoftDeletes` (alias `softDeletable`) | `deleted_at` plus soft-delete query scopes |
101
- | `useAuth` (alias `authenticatable`) | Auth columns; `{ usePasskey: true }` adds passkeys |
101
+ | `useAuth` (alias `authenticatable`) | Auth columns; `{ usePasskey: true }` adds passkeys. Also confers `morphMany: { tokenable: 'PersonalAccessToken' }`, so any authenticatable model can hold API tokens |
102
102
  | `useApi` | Generates REST actions and routes: `{ uri, routes, middleware? }` |
103
103
  | `useSearch` (alias `searchable`) | Search-engine indexing: `{ displayable, searchable, sortable, filterable }` |
104
104
  | `useSocials` | OAuth identities, e.g. `['github']` |
105
+ | `useActivityLog` | Writes an `activities` feed row per change: `{ logOnly }` / `{ include }` / `{ exclude }` pick the attributes |
106
+ | `useAudit` | Writes a `model_audits` row per change with an old/new diff |
105
107
  | `observe` | Emits `{model}:created` / `:updated` / `:deleted` events |
106
108
  | `billable` | Stripe methods (`checkout()`, `activeSubscription()`, ...) |
107
109
  | `taggable` / `categorizable` / `commentable` / `likeable` | Pivot tables and their relation methods |
@@ -11,7 +11,7 @@ allowed-tools: Read Edit Write Bash Grep Glob
11
11
  ## Key Paths
12
12
  - Core ORM package: `storage/framework/core/orm/src/`
13
13
  - ORM implementation: `storage/framework/orm/`
14
- - Model definitions: `storage/framework/defaults/app/Models/` (97 models)
14
+ - Model definitions: `storage/framework/defaults/app/Models/` (98 models)
15
15
  - Application models: `app/Models/`
16
16
  - Default model templates: `storage/framework/defaults/app/Models/`
17
17
  - ORM type globals: `storage/framework/types/orm-globals.d.ts`
@@ -16,7 +16,7 @@ export default new Middleware({
16
16
  name: 'api',
17
17
  priority: 1,
18
18
 
19
- async handle(request) {
19
+ handle(request) {
20
20
  // Ensure the request accepts JSON
21
21
  const accept = request.headers.get('accept') || ''
22
22
  if (!accept.includes('application/json') && !accept.includes('*/*')) {
@@ -1,4 +1,6 @@
1
- import { Auth, authCookieName, sessionUser } from '@stacksjs/auth'
1
+ import { Auth } from '@stacksjs/auth/authentication'
2
+ import { authCookieName } from '@stacksjs/auth/cookie'
3
+ import { sessionUser } from '@stacksjs/auth/session-auth'
2
4
  import { HttpError } from '@stacksjs/error-handling'
3
5
  import { Middleware } from '@stacksjs/router'
4
6
 
@@ -1,4 +1,5 @@
1
- import { authenticatedUser, AuthorizationException, authorize } from '@stacksjs/auth'
1
+ import { AuthorizationException, authorize } from '@stacksjs/auth/gate'
2
+ import { authenticatedUser } from '@stacksjs/auth/middleware'
2
3
  import { HttpError } from '@stacksjs/error-handling'
3
4
  import { Middleware, resolveRouteModel, setRouteModelFallback } from '@stacksjs/router'
4
5
 
@@ -37,7 +37,7 @@ import { Middleware } from '@stacksjs/router'
37
37
  * - **Skip** for already-compressed content types (image/*, video/*,
38
38
  * audio/*, application/zip, gzip, octet-stream).
39
39
  * - **Skip** SSE / WebSocket upgrades (`text/event-stream`).
40
- * - **Prefer brotli** when `br` is offered. Brotli typically
40
+ * - **Prefer the highest accepted weight**, using brotli on ties. Brotli typically
41
41
  * achieves ~20% better ratios than gzip on HTML/JSON/CSS/JS at
42
42
  * comparable CPU cost — well worth the swap when the client
43
43
  * supports it.
@@ -75,24 +75,66 @@ const SKIP_CONTENT_TYPE_PREFIXES = [
75
75
  'text/event-stream',
76
76
  ]
77
77
 
78
- /** Parse the q-weighted Accept-Encoding header into a preference set. */
78
+ type Encoding = 'br' | 'gzip' | null
79
+
80
+ let lastAcceptEncoding: string | null | undefined
81
+ let lastEncoding: Encoding = null
82
+
83
+ /** Repeated offers reuse one pure result, without retaining a growing cache. */
79
84
  function pickEncoding(acceptEncoding: string | null): 'br' | 'gzip' | null {
80
- if (!acceptEncoding) return null
81
- // We don't fully implement q-value parsing — it's overkill for the
82
- // brotli-vs-gzip choice. We just check for token presence.
83
- const lc = acceptEncoding.toLowerCase()
84
- // Brotli first because it ratios better on text — typical 20% smaller
85
- // than gzip on HTML/JSON. CPU cost per byte is similar at default
86
- // quality on Node's libbrotli.
87
- if (lc.includes('br')) return 'br'
88
- if (lc.includes('gzip')) return 'gzip'
85
+ if (acceptEncoding === lastAcceptEncoding)
86
+ return lastEncoding
87
+ const encoding = parseEncoding(acceptEncoding)
88
+ lastAcceptEncoding = acceptEncoding
89
+ lastEncoding = encoding
90
+ return encoding
91
+ }
92
+
93
+ /** Exact HTTP coding tokens and qualities, with Brotli first on equal weights. */
94
+ function parseEncoding(header: string | null): Encoding {
95
+ if (!header)
96
+ return null
97
+
98
+ let br: number | undefined
99
+ let gzip: number | undefined
100
+ let identity = 0
101
+ let wildcard = 0
102
+ for (const part of header.split(',')) {
103
+ const separator = part.indexOf(';')
104
+ const name = (separator < 0 ? part : part.slice(0, separator)).trim().toLowerCase()
105
+ if (name !== 'br' && name !== 'gzip' && name !== 'identity' && name !== '*')
106
+ continue
107
+
108
+ let quality = 1
109
+ if (separator >= 0) {
110
+ const weight = /^;[\t ]*q=(0(?:\.\d{0,3})?|1(?:\.0{0,3})?)[\t ]*$/i.exec(part.slice(separator))
111
+ quality = weight ? Number(weight[1]) : 0
112
+ }
113
+ if (name === 'br')
114
+ br = quality
115
+ else if (name === 'gzip')
116
+ gzip = quality
117
+ else if (name === 'identity')
118
+ identity = quality
119
+ else
120
+ wildcard = quality
121
+ }
122
+ br ??= wildcard
123
+ gzip ??= wildcard
124
+ if (identity > Math.max(br, gzip))
125
+ return null
126
+ if (br > 0 && br >= gzip)
127
+ return 'br'
128
+ if (gzip > 0)
129
+ return 'gzip'
89
130
  return null
90
131
  }
91
132
 
92
133
  /** Heuristic: is this content type worth compressing? */
93
134
  function shouldCompressContentType(contentType: string | null): boolean {
94
135
  if (!contentType) return true // unknown → assume text-like
95
- const ct = (contentType.toLowerCase().split(';')[0] ?? '').trim()
136
+ const separator = contentType.indexOf(';')
137
+ const ct = (separator < 0 ? contentType : contentType.slice(0, separator)).trim().toLowerCase()
96
138
  for (const prefix of SKIP_CONTENT_TYPE_PREFIXES) {
97
139
  if (ct.startsWith(prefix)) return false
98
140
  }
@@ -109,6 +151,9 @@ function shouldCompressContentType(contentType: string | null): boolean {
109
151
  * JSON/HTML response sizes we're targeting (KB to single-digit MB).
110
152
  * For multi-MB streamed responses, the middleware should be skipped
111
153
  * (the SSE / event-stream check above handles the most common case).
154
+ * A caller that already knows a conservative UTF-8 byte upper bound may
155
+ * supply it to leave a proven sub-threshold body unconsumed. This must come
156
+ * from the body itself, not an unverified Content-Length header.
112
157
  *
113
158
  * @example
114
159
  * ```ts
@@ -117,11 +162,14 @@ function shouldCompressContentType(contentType: string | null): boolean {
117
162
  * // → response with `Content-Encoding: br` and a brotli-compressed body
118
163
  * ```
119
164
  */
120
- export async function applyCompression(request: Request, response: Response): Promise<Response> {
165
+ export async function applyCompression(request: Request, response: Response, bodySizeUpperBound?: number): Promise<Response> {
121
166
  // Already encoded — leave it alone. Re-encoding would corrupt the
122
167
  // body, and the client already knows what to expect.
123
168
  if (response.headers.get('content-encoding')) return response
124
169
 
170
+ if (bodySizeUpperBound !== undefined && Number.isSafeInteger(bodySizeUpperBound) && bodySizeUpperBound >= 0 && bodySizeUpperBound < MIN_COMPRESS_BYTES)
171
+ return response
172
+
125
173
  // No body or non-text content streams (SSE, etc.) — skip.
126
174
  if (!response.body) return response
127
175
 
@@ -145,7 +193,7 @@ export async function applyCompression(request: Request, response: Response): Pr
145
193
  })
146
194
  }
147
195
 
148
- let compressed: Buffer
196
+ let compressed: Uint8Array<ArrayBuffer>
149
197
  if (encoding === 'br') {
150
198
  // Brotli quality 5 is the sweet spot for dynamic responses:
151
199
  // quality 11 (max) is ~10× slower for ~5% smaller output.
@@ -161,7 +209,7 @@ export async function applyCompression(request: Request, response: Response): Pr
161
209
  // for typical response sizes — used here in preference to
162
210
  // node:zlib for the gzip path. Brotli has no Bun-native API yet
163
211
  // (as of writing) so we use node:zlib for that branch.
164
- compressed = Buffer.from(Bun.gzipSync(bodyBuf))
212
+ compressed = Bun.gzipSync(bodyBuf)
165
213
  }
166
214
 
167
215
  const newHeaders = new Headers(response.headers)
@@ -180,9 +228,8 @@ export async function applyCompression(request: Request, response: Response): Pr
180
228
  newHeaders.set('Vary', 'Accept-Encoding')
181
229
  }
182
230
 
183
- // Node's Buffer satisfies BodyInit at runtime but not in the DOM lib's
184
- // types; the underlying bytes are what Response wants either way.
185
- return new Response(new Uint8Array(compressed), {
231
+ // Both codecs return owned, non-shared bytes accepted directly by Response.
232
+ return new Response(compressed, {
186
233
  status: response.status,
187
234
  statusText: response.statusText,
188
235
  headers: newHeaders,
@@ -207,7 +254,7 @@ export default new Middleware({
207
254
  // to the action — the post-action wrapper picks it up by name.
208
255
  priority: 100,
209
256
 
210
- async handle(request) {
257
+ handle(request) {
211
258
  // Stamp the request so the post-response wrapper knows to compress.
212
259
  // This is read by the router's response-finalization step.
213
260
  ;request._compress = true
@@ -294,7 +294,7 @@ export default new Middleware({
294
294
  // actual auth failure).
295
295
  priority: 0,
296
296
 
297
- async handle(request) {
297
+ handle(request) {
298
298
  const cfg = resolveCorsConfig()
299
299
 
300
300
  // Preflight short-circuit — throwing a Response is the documented way
@@ -1,6 +1,6 @@
1
1
  import { timingSafeEqual } from 'node:crypto'
2
2
  import { HttpError } from '@stacksjs/error-handling'
3
- import type { EnhancedRequest } from '@stacksjs/bun-router'
3
+ import type { EnhancedRequest } from '@stacksjs/router'
4
4
  import { Middleware } from '@stacksjs/router'
5
5
 
6
6
  /**
@@ -63,6 +63,8 @@ const CSRF_SECURE_TRANSPORT = Symbol.for('@stacksjs/router:csrf-secure-transport
63
63
  const CSRF_HEADER_NAME = 'x-csrf-token'
64
64
  const CSRF_COOKIE_PREFIX = `${CSRF_COOKIE_NAME}=`
65
65
  const LEGACY_CSRF_COOKIE_PREFIX = 'csrf-token='
66
+ const JOINED_CSRF_COOKIE_PREFIX = `, ${CSRF_COOKIE_PREFIX}`
67
+ const JOINED_LEGACY_CSRF_COOKIE_PREFIX = `, ${LEGACY_CSRF_COOKIE_PREFIX}`
66
68
  const CSRF_COOKIE_SUFFIX = '; Path=/; SameSite=Lax; Max-Age=7200'
67
69
  const CSRF_COOKIE_SECURE_SUFFIX = `${CSRF_COOKIE_SUFFIX}; Secure`
68
70
  const TOKEN_BYTES = 32
@@ -122,12 +124,15 @@ function responseAlreadySeeds(response: Response): boolean {
122
124
  ? headers.getSetCookie()
123
125
  : [headers.get('set-cookie') || '']
124
126
 
125
- return cookies.some(cookie =>
126
- cookie.startsWith(`${CSRF_COOKIE_NAME}=`)
127
- || cookie.startsWith('csrf-token=')
128
- || cookie.includes(`, ${CSRF_COOKIE_NAME}=`)
129
- || cookie.includes(', csrf-token='),
130
- )
127
+ for (const cookie of cookies) {
128
+ if (
129
+ cookie.startsWith(CSRF_COOKIE_PREFIX)
130
+ || cookie.startsWith(LEGACY_CSRF_COOKIE_PREFIX)
131
+ || cookie.includes(JOINED_CSRF_COOKIE_PREFIX)
132
+ || cookie.includes(JOINED_LEGACY_CSRF_COOKIE_PREFIX)
133
+ ) return true
134
+ }
135
+ return false
131
136
  }
132
137
 
133
138
  export function createCsrfCookie(req: Request, minted?: string): string {
@@ -199,14 +204,26 @@ function csrfCookieToken(req: Request): string {
199
204
  }
200
205
  let canonical = ''
201
206
  let legacy = ''
202
- for (const part of header.split(';')) {
203
- const idx = part.indexOf('=')
204
- if (idx === -1) continue
205
- const name = part.slice(0, idx).trim()
207
+ let start = 0
208
+ while (start < header.length) {
209
+ // Scan each name only once, stopping at '=' or a malformed pair's ';'.
210
+ let equals = start
211
+ while (equals < header.length && header.charCodeAt(equals) !== 61 && header.charCodeAt(equals) !== 59)
212
+ equals++
213
+ if (equals === header.length)
214
+ break
215
+ if (header.charCodeAt(equals) === 59) {
216
+ start = equals + 1
217
+ continue
218
+ }
219
+ const separator = header.indexOf(';', equals + 1)
220
+ const end = separator === -1 ? header.length : separator
221
+ const name = header.slice(start, equals).trim()
206
222
  if (name === CSRF_COOKIE_NAME)
207
- canonical = part.slice(idx + 1).trim()
223
+ canonical = header.slice(equals + 1, end).trim()
208
224
  else if (name === 'csrf-token')
209
- legacy = part.slice(idx + 1).trim()
225
+ legacy = header.slice(equals + 1, end).trim()
226
+ start = end + 1
210
227
  }
211
228
  return canonical || legacy
212
229
  }
@@ -1,4 +1,4 @@
1
- import { authenticatedUser } from '@stacksjs/auth'
1
+ import { authenticatedUser } from '@stacksjs/auth/middleware'
2
2
  import { HttpError } from '@stacksjs/error-handling'
3
3
  import { Middleware } from '@stacksjs/router'
4
4
 
@@ -1,5 +1,7 @@
1
1
  import { Middleware } from '@stacksjs/router'
2
2
 
3
+ let serverModule: typeof import('@stacksjs/server') | undefined
4
+
3
5
  /**
4
6
  * Maintenance / Coming-Soon Middleware
5
7
  *
@@ -27,7 +29,7 @@ export default new Middleware({
27
29
  priority: 0, // Run first, before all other middleware
28
30
 
29
31
  async handle(request) {
30
- const { maintenanceGate } = await import('@stacksjs/server')
32
+ const { maintenanceGate } = serverModule ??= await import('@stacksjs/server')
31
33
 
32
34
  const gated = await maintenanceGate(request)
33
35
  if (gated)
@@ -1,4 +1,4 @@
1
- import { authenticatedUser } from '@stacksjs/auth'
1
+ import { authenticatedUser } from '@stacksjs/auth/middleware'
2
2
  import { HttpError } from '@stacksjs/error-handling'
3
3
  import { Middleware } from '@stacksjs/router'
4
4
 
@@ -32,7 +32,7 @@ export default new Middleware({
32
32
  throw new HttpError(401, 'Unauthenticated.')
33
33
  }
34
34
 
35
- const { hasAnyPermission } = await import('@stacksjs/auth')
35
+ const { hasAnyPermission } = await import('@stacksjs/auth/rbac')
36
36
 
37
37
  const hasRequired = await hasAnyPermission(user, requiredPermissions)
38
38
 
@@ -1,4 +1,4 @@
1
- import { authenticatedUser } from '@stacksjs/auth'
1
+ import { authenticatedUser } from '@stacksjs/auth/middleware'
2
2
  import { HttpError } from '@stacksjs/error-handling'
3
3
  import { Middleware } from '@stacksjs/router'
4
4
 
@@ -32,7 +32,7 @@ export default new Middleware({
32
32
  }
33
33
 
34
34
  // Dynamically import to avoid circular dependency
35
- const { hasAnyRole } = await import('@stacksjs/auth')
35
+ const { hasAnyRole } = await import('@stacksjs/auth/rbac')
36
36
 
37
37
  const hasRequired = await hasAnyRole(user, requiredRoles)
38
38
 
@@ -1,4 +1,4 @@
1
- import { authenticatedUser } from '@stacksjs/auth'
1
+ import { authenticatedUser } from '@stacksjs/auth/middleware'
2
2
  import { HttpError } from '@stacksjs/error-handling'
3
3
  import { Middleware } from '@stacksjs/router'
4
4
 
@@ -67,7 +67,6 @@ export default new Middleware({
67
67
  // If result is a Response (429), throw it to short-circuit the request
68
68
  if (result instanceof Response) {
69
69
  // Transform the response to match stacks format
70
- const _body = await result.clone().json().catch(() => ({}))
71
70
  const retryAfter = result.headers.get('Retry-After') || '60'
72
71
 
73
72
  throw new Response(JSON.stringify({
@@ -45,6 +45,7 @@ export default defineModel({
45
45
  observe: true,
46
46
  },
47
47
 
48
+
48
49
  hasMany: ['Post'],
49
50
  belongsTo: ['User'],
50
51
 
@@ -15,10 +15,12 @@ export default defineModel({
15
15
  useUuid: true,
16
16
  useTimestamps: true,
17
17
  useSearch: {
18
- displayable: ['id', 'title', 'slug', 'author', 'views', 'status', 'poster', 'focusKeyword', 'metaDescription', 'canonicalUrl'],
18
+ displayable: ['id', 'title', 'slug', 'authorId', 'views', 'status', 'poster', 'focusKeyword', 'metaDescription', 'canonicalUrl'],
19
19
  // `content`, not `body` - the column is `content`, and the old spelling
20
- // silently indexed nothing. `comments` likewise was never a column.
21
- searchable: ['title', 'slug', 'author', 'content', 'excerpt', 'focusKeyword', 'metaDescription'],
20
+ // silently indexed nothing. `comments` likewise was never a column, and
21
+ // nor was `author`: the column is `author_id`, which is worth showing and
22
+ // filtering on but is not free text to search.
23
+ searchable: ['title', 'slug', 'content', 'excerpt', 'focusKeyword', 'metaDescription'],
22
24
  sortable: ['published_at', 'views'],
23
25
  filterable: ['status'],
24
26
  },
@@ -0,0 +1,154 @@
1
+ import { defineModel, selfOwnership } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ /**
5
+ * A named API token belonging to a user - the Sanctum shape.
6
+ *
7
+ * Maps the table the auth layer already owns rather than introducing a second
8
+ * one. `@stacksjs/auth` issues these through `createToken(userId, name, scopes)`
9
+ * and checks them with `tokenCan` / `tokenCanAll` / `tokenAbilities`; what was
10
+ * missing was the model, so `User.hasMany('PersonalAccessToken')` named
11
+ * something real and a token list could be queried, related and exposed like
12
+ * any other row.
13
+ *
14
+ * `oauth_access_tokens` is created by `ensureAuthTables()` rather than by a
15
+ * migration, the same as `notifications` - which is why nothing here declares
16
+ * columns the auth layer does not already define.
17
+ *
18
+ * The token column holds a HASH, never the plaintext: `createToken` returns the
19
+ * plaintext once, to the caller, and it is not recoverable afterwards. It is
20
+ * `hidden` so it cannot leave through a serialised response, and `guarded` so
21
+ * no mass assignment can set it.
22
+ */
23
+ export default defineModel({
24
+ name: 'PersonalAccessToken',
25
+ table: 'oauth_access_tokens',
26
+ primaryKey: 'id',
27
+ autoIncrement: true,
28
+
29
+ traits: {
30
+ useTimestamps: true,
31
+ // Deliberately no `useApi`.
32
+ //
33
+ // Minting and revoking both have semantics a generic CRUD route does not
34
+ // carry. `createToken` returns the plaintext exactly once, so a generic
35
+ // `store` would write a row nobody holds the token for; and revocation has
36
+ // to take the paired refresh token with it (stacksjs/stacks#2306), which a
37
+ // row delete does not. `@stacksjs/auth` owns both operations.
38
+ //
39
+ // It also keeps the write surface honest: row scoping compares ONE field,
40
+ // and this table's owner is a `tokenable_type` / `tokenable_id` pair - so a
41
+ // generated `destroy` scoped to `tokenable_id` alone would let a user
42
+ // destroy an author's token whenever the two ids happened to match.
43
+ },
44
+
45
+ /**
46
+ * Rows belong to whoever the pair names. Declared even with no generated
47
+ * routes, so adding `useApi` later cannot ship an unscoped write by omission
48
+ * (stacksjs/stacks#2375) - though see the note above about the one field this
49
+ * can express.
50
+ */
51
+ ownership: selfOwnership('tokenable_id'),
52
+
53
+ // No `belongsTo`: the owner is polymorphic. `tokenable_type` names the
54
+ // owner's table, so a users-only foreign key would be a claim the column no
55
+ // longer makes - and the FK preflight reported exactly that once the legacy
56
+ // `user_id` stopped being the owner.
57
+ attributes: {
58
+ /**
59
+ * The owner: the table it lives in, and its id there.
60
+ *
61
+ * Declared so the generated row type carries them. They are written by
62
+ * `@stacksjs/auth` when a token is minted and never by a caller, so both
63
+ * are guarded - an owner a request could set is an owner a request could
64
+ * change.
65
+ */
66
+ tokenableType: {
67
+ order: 1,
68
+ guarded: true,
69
+ validation: {
70
+ rule: schema.string().max(255),
71
+ },
72
+ factory: () => 'users',
73
+ },
74
+
75
+ tokenableId: {
76
+ order: 2,
77
+ guarded: true,
78
+ validation: {
79
+ rule: schema.number(),
80
+ },
81
+ factory: () => 1,
82
+ },
83
+
84
+ name: {
85
+ order: 3,
86
+ fillable: true,
87
+ validation: {
88
+ rule: schema.string().max(255),
89
+ },
90
+ factory: faker => `${faker.hacker.noun()}-token`,
91
+ },
92
+
93
+ token: {
94
+ order: 4,
95
+ // The stored hash. Never serialised, never mass-assignable.
96
+ hidden: true,
97
+ guarded: true,
98
+ validation: {
99
+ rule: schema.string(),
100
+ },
101
+ },
102
+
103
+ scopes: {
104
+ order: 5,
105
+ type: 'text',
106
+ fillable: true,
107
+ validation: {
108
+ rule: schema.string(),
109
+ },
110
+ factory: () => JSON.stringify(['*']),
111
+ },
112
+
113
+ revoked: {
114
+ order: 6,
115
+ fillable: true,
116
+ default: false,
117
+ validation: {
118
+ rule: schema.boolean(),
119
+ },
120
+ factory: () => false,
121
+ },
122
+
123
+ expiresAt: {
124
+ order: 7,
125
+ fillable: true,
126
+ validation: {
127
+ rule: schema.string(),
128
+ },
129
+ },
130
+
131
+ /**
132
+ * What the browser called itself, and where it came from.
133
+ *
134
+ * So a person can recognise their own sessions on a list well enough to
135
+ * revoke one. Untrusted - a client chooses the string - which is why
136
+ * nothing authorises on either.
137
+ */
138
+ userAgent: {
139
+ order: 8,
140
+ fillable: true,
141
+ validation: {
142
+ rule: schema.string().max(255),
143
+ },
144
+ },
145
+
146
+ ipAddress: {
147
+ order: 9,
148
+ fillable: true,
149
+ validation: {
150
+ rule: schema.string().max(45),
151
+ },
152
+ },
153
+ },
154
+ } as const)
@@ -66,10 +66,10 @@ export default defineModel({
66
66
  hasOne: ['Subscriber', 'Courier', 'Author'],
67
67
 
68
68
  hasMany: [
69
- 'PersonalAccessToken',
70
69
  'Customer',
71
70
  'TeamMember',
72
71
  ],
72
+
73
73
  attributes: {
74
74
  name: {
75
75
  order: 2,
@@ -16,10 +16,10 @@ export default defineModel({
16
16
  useUuid: true,
17
17
  useTimestamps: true,
18
18
  useSearch: {
19
- displayable: ['id', 'cartId', 'productId', 'quantity', 'unitPrice', 'totalPrice'],
20
- searchable: ['id', 'cartId', 'productId'],
19
+ displayable: ['id', 'cartId', 'productName', 'productSku', 'quantity', 'unitPrice', 'totalPrice'],
20
+ searchable: ['id', 'cartId', 'productName', 'productSku'],
21
21
  sortable: ['createdAt', 'updatedAt', 'quantity', 'unitPrice'],
22
- filterable: ['cartId', 'productId'],
22
+ filterable: ['cartId', 'productSku'],
23
23
  },
24
24
 
25
25
  useSeeder: {
@@ -16,10 +16,10 @@ export default defineModel({
16
16
  useUuid: true,
17
17
  useTimestamps: true,
18
18
  useSearch: {
19
- displayable: ['id', 'orderId', 'customerId', 'amount', 'method', 'status', 'date'],
19
+ displayable: ['id', 'orderId', 'customerId', 'amount', 'method', 'status', 'createdAt'],
20
20
  searchable: ['orderId', 'customerId', 'referenceNumber'],
21
21
  sortable: ['amount', 'createdAt'],
22
- filterable: ['method', 'status', 'date'],
22
+ filterable: ['method', 'status', 'createdAt'],
23
23
  },
24
24
 
25
25
  useSeeder: {
@@ -16,9 +16,9 @@ export default defineModel({
16
16
  useUuid: true,
17
17
  useTimestamps: true,
18
18
  useSearch: {
19
- displayable: ['id', 'name', 'email', 'phone', 'partySize', 'notificationPreference', 'source', 'notes', 'status'],
19
+ displayable: ['id', 'name', 'email', 'phone', 'quantity', 'notificationPreference', 'source', 'notes', 'status'],
20
20
  searchable: ['name', 'email', 'phone', 'source'],
21
- sortable: ['name', 'partySize', 'createdAt', 'updatedAt', 'status'],
21
+ sortable: ['name', 'quantity', 'createdAt', 'updatedAt', 'status'],
22
22
  filterable: ['notificationPreference', 'source', 'status'],
23
23
  },
24
24
 
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.74.29",
5
+ "version": "0.74.31",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.74.29",
5
+ "version": "0.74.31",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@iconify-json/f7": "^1.2.2",
57
57
  "@iconify-json/hugeicons": "^1.2.27",
58
- "@stacksjs/mobile": "^0.74.29",
58
+ "@stacksjs/mobile": "^0.74.31",
59
59
  "@stacksjs/sanitizer": "^0.2.113",
60
60
  "ts-qr-codes": "^0.1.8"
61
61
  }