@stacksjs/defaults 0.75.15 → 0.75.17

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.
@@ -35,8 +35,9 @@ const head = getAnalyticsHead(analytics)
35
35
  ```
36
36
 
37
37
  The registry is loud on purpose. A `driver` value with no implementation throws,
38
- and so does a driver that is selected but missing its config (the message names
39
- the exact key to set). Only an app with no `driver` at all gets an empty result.
38
+ and so does a selected third-party driver missing its config (the message names
39
+ the exact key to set). An app with no `driver` defaults to AnalyticsHQ. It emits
40
+ nothing until `drivers.analyticshq.siteId` is set.
40
41
 
41
42
  Nothing injects the script for you - a framework that silently posted pageviews
42
43
  to a third party would be the wrong default. Render it yourself in the layout
@@ -142,9 +143,9 @@ interface SelfHostedConfig {
142
143
 
143
144
  ```typescript
144
145
  {
145
- driver: 'fathom', // 'fathom' | 'plausible' | 'google-analytics' | 'self-hosted' | 'analyticshq'
146
+ driver: 'analyticshq', // 'analyticshq' | 'fathom' | 'plausible' | 'google-analytics' | 'self-hosted'
146
147
  drivers: {
147
- analyticshq: { siteId: '' },
148
+ analyticshq: { siteId: '' }, // empty means no tracking tags
148
149
  googleAnalytics: { trackingId: '' },
149
150
  fathom: { siteId: '' },
150
151
  plausible: { domain: '' },
@@ -169,7 +170,8 @@ Analytics dashboard at `/dashboard/analytics` displays:
169
170
 
170
171
  ## Gotchas
171
172
  - Selecting a driver does not inject anything - call `generateAnalyticsScript()` in your layout
172
- - A misconfigured driver throws rather than emitting nothing; the message names the config key
173
+ - A misconfigured third-party driver throws rather than emitting nothing; the message names the config key
174
+ - AnalyticsHQ is the first-party default and emits nothing until its site id is set
173
175
  - `honorDnt: true` respects browser Do Not Track settings
174
176
  - Attribute values are escaped, and inline script values are JS-escaped, to prevent XSS
175
177
  - Fathom is a paid service - self-hosted is free but requires infrastructure
@@ -75,7 +75,8 @@ The `PersonalAccessToken` model maps the same table, so `owner.with('tokenable')
75
75
  lists exactly what `createToken` minted. It deliberately generates no CRUD
76
76
  routes: minting and revoking both carry semantics a generic route does not.
77
77
  - `Auth.once(credentials: AuthCredentials): Promise<boolean>` — one-time auth without token
78
- - `Auth.requestToken(credentials, clientId, clientSecret): Promise<{ token } | null>` — OAuth token request
78
+ - `Auth.requestUserTokenWithClient(credentials, clientId, clientSecret): Promise<{ token } | null>` authenticates both a legacy OAuth client and an end user password, then issues a user token. It is not the client credentials grant.
79
+ - `Auth.requestToken(...)` is the deprecated compatibility alias for that same legacy exchange. New delegated integrations use the authorization-code provider.
79
80
 
80
81
  ### User State
81
82
  - `Auth.user(): Promise<UserModel | undefined>` — get authenticated user from bearer token
@@ -155,6 +156,19 @@ routes: minting and revoking both carry semantics a generic route does not.
155
156
  - `createClient(options: CreateClientOptions): Promise<CreateClientResult>`
156
157
  - `revokeClient(clientId): Promise<void>`
157
158
 
159
+ ### OAuth Provider and PKCE
160
+
161
+ The authorization server is opt-in through `config/auth.ts` under
162
+ `oauthProvider`. It is separate from social sign-in, where Stacks is the OAuth
163
+ client. The provider profile is Authorization Code with S256 PKCE and rotating
164
+ refresh tokens. It does not support implicit or password grants.
165
+
166
+ - `resolveOAuthProviderConfig(options)` returns `null` unless explicitly enabled
167
+ - `generatePkceVerifier()` creates a 256-bit RFC 7636 verifier
168
+ - `createS256CodeChallenge(verifier)` derives its S256 challenge
169
+ - `verifyS256CodeChallenge(verifier, challenge)` validates without a plain fallback
170
+ - `isValidPkceVerifier(value)` checks the required 43 to 128 character syntax
171
+
158
172
  ## Two-Factor Authentication (authenticator.ts)
159
173
 
160
174
  - `generateTwoFactorSecret(): string`
@@ -77,9 +77,7 @@ bcryptEncode(value, rounds?) // bcrypt hash
77
77
  bcryptVerify(value, hash) // bcrypt verify
78
78
  argon2Encode(value, options?) // argon2 hash
79
79
  argon2Verify(value, hash) // argon2 verify
80
- md5Encode(value) // MD5 (legacy only)
81
80
  base64Encode(value) // base64
82
- base64Verify(value, encoded) // base64 compare
83
81
  ```
84
82
 
85
83
  ## HashMakeOptions
@@ -124,6 +122,7 @@ interface HashMakeOptions {
124
122
  - APP_KEY is used for `encrypt()`/`decrypt()` — generate via `buddy key:generate`
125
123
  - APP_KEY format is colon-separated (validated during deployment)
126
124
  - Argon2 requires more memory/time but is more resistant to GPU attacks
127
- - `md5Encode()` exists for legacy compatibility only — never use for passwords
125
+ - Base64 is encoding, not hashing. Never use `base64Encode()` for credentials.
126
+ - MD5 and base64 password verification are intentionally absent from the main security API.
128
127
  - The firewall config is used by cloud deployment for WAF rules
129
128
  - Rate limiting is per-minute per-IP (500 default)
@@ -309,7 +309,7 @@ Convention over configuration, while staying wholly configurable. _No more boile
309
309
  - 🔤 **Spell Checker** _be notified once there are typos_
310
310
  - 🛠️ **Essential Utilities** _powers at your fingertips. Collections, STX composables, and more_
311
311
  - 👥 **Team Management** _manage your team & their permissions_
312
- - 🧪 **Streamlined Testing** _unit & e2e tests powered by Bun, Vitest & Playwright_
312
+ - 🧪 **Streamlined Testing** _unit, feature & component tests powered by Bun & very-happy-dom_
313
313
 
314
314
  No matter whether you are a beginner or an expert, the approachable Stacks design allows you to learn at your own pace, using our thorough documentation covering every aspect of the framework. Stacks is extremely beginner & expert-friendly.
315
315
 
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.15",
5
+ "version": "0.75.17",
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.75.15",
5
+ "version": "0.75.17",
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.75.15",
58
+ "@stacksjs/mobile": "^0.75.17",
59
59
  "@stacksjs/sanitizer": "^0.2.113",
60
60
  "ts-qr-codes": "^0.1.8"
61
61
  }
@@ -6,6 +6,49 @@
6
6
  * automatically be injected into the Bun process.
7
7
  */
8
8
 
9
+ /**
10
+ * Every global the runtime defined before this file assigned any.
11
+ *
12
+ * Taken first, at module load, because the auto-import loops below write
13
+ * framework exports onto globalThis, and a name the runtime already owns must
14
+ * never be one of them. A hand-kept list of such names is how `Worker` slipped
15
+ * through: `@stacksjs/queue` exports a queue `Worker`, it replaced Bun's Web
16
+ * Worker in every Stacks process, and anything calling `new Worker(url)` built
17
+ * a queue worker instead - stx's image warm-up failed silently, an app's own
18
+ * syntax-highlighter worker broke. `reportError` from `@stacksjs/validation`
19
+ * was overwriting the web `reportError` the same way.
20
+ */
21
+ const HOST_GLOBALS: ReadonlySet<string> = new Set(Object.getOwnPropertyNames(globalThis))
22
+
23
+ /**
24
+ * Names never auto-imported onto globalThis, even when the runtime does not
25
+ * define them: another runtime's globals (`window`, `Deno`), module-scope
26
+ * bindings (`require`, `__dirname`), and the ones listed long before the
27
+ * snapshot above existed, kept so nothing that relied on them changes.
28
+ */
29
+ const RESERVED_GLOBALS: ReadonlySet<string> = new Set([
30
+ 'process', 'globalThis', 'global', 'window', 'self',
31
+ 'console', 'require', 'module', 'exports', '__dirname', '__filename',
32
+ 'Buffer', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
33
+ 'setImmediate', 'clearImmediate', 'queueMicrotask',
34
+ 'fetch', 'Request', 'Response', 'Headers', 'URL', 'URLSearchParams',
35
+ 'TextEncoder', 'TextDecoder', 'Blob', 'File', 'FormData',
36
+ 'crypto', 'performance', 'navigator', 'location',
37
+ 'Promise', 'Symbol', 'Proxy', 'Reflect', 'WeakMap', 'WeakSet', 'Map', 'Set',
38
+ 'Array', 'Object', 'String', 'Number', 'Boolean', 'Date', 'RegExp', 'Error',
39
+ 'JSON', 'Math', 'Intl', 'eval', 'isNaN', 'isFinite', 'parseInt', 'parseFloat',
40
+ 'encodeURI', 'encodeURIComponent', 'decodeURI', 'decodeURIComponent',
41
+ 'Bun', 'Deno', 'Node',
42
+ ])
43
+
44
+ /**
45
+ * Whether an auto-import must leave `name` alone: the runtime already defines
46
+ * it, or it is reserved. `hostGlobals` is injectable for tests.
47
+ */
48
+ export function isProtectedGlobal(name: string, hostGlobals: ReadonlySet<string> = HOST_GLOBALS): boolean {
49
+ return RESERVED_GLOBALS.has(name) || hostGlobals.has(name)
50
+ }
51
+
9
52
  // Skip preloader for fast CLI commands (e.g. `buddy dev`, `buddy --version`) to
10
53
  // maximize startup speed. We must NOT skip when running a server script directly
11
54
  // (e.g. `bun --watch storage/framework/core/actions/src/dev/api.ts`) — Bun
@@ -214,22 +257,6 @@ export async function loadAutoImports() {
214
257
  const path = await import('../../../core/path/src/index.ts')
215
258
  .catch(() => import(pathPackage))
216
259
 
217
- // CRITICAL: Never overwrite these built-in globals
218
- const protectedGlobals = new Set([
219
- 'process', 'globalThis', 'global', 'window', 'self',
220
- 'console', 'require', 'module', 'exports', '__dirname', '__filename',
221
- 'Buffer', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
222
- 'setImmediate', 'clearImmediate', 'queueMicrotask',
223
- 'fetch', 'Request', 'Response', 'Headers', 'URL', 'URLSearchParams',
224
- 'TextEncoder', 'TextDecoder', 'Blob', 'File', 'FormData',
225
- 'crypto', 'performance', 'navigator', 'location',
226
- 'Promise', 'Symbol', 'Proxy', 'Reflect', 'WeakMap', 'WeakSet', 'Map', 'Set',
227
- 'Array', 'Object', 'String', 'Number', 'Boolean', 'Date', 'RegExp', 'Error',
228
- 'JSON', 'Math', 'Intl', 'eval', 'isNaN', 'isFinite', 'parseInt', 'parseFloat',
229
- 'encodeURI', 'encodeURIComponent', 'decodeURI', 'decodeURIComponent',
230
- 'Bun', 'Deno', 'Node',
231
- ])
232
-
233
260
  // 1. Load Stacks framework packages into globalThis
234
261
  const stacksPackages = [
235
262
  '@stacksjs/actions',
@@ -275,8 +302,8 @@ export async function loadAutoImports() {
275
302
  try {
276
303
  const module = await import(pkg)
277
304
  for (const [name, value] of Object.entries(module)) {
278
- // Skip default exports and protected globals
279
- if (name === 'default' || protectedGlobals.has(name)) continue
305
+ // Skip default exports and anything the runtime already owns
306
+ if (name === 'default' || isProtectedGlobal(name)) continue
280
307
  if (typeof value !== 'undefined') {
281
308
  Reflect.set(globalThis, name, value)
282
309
  }
@@ -295,8 +322,8 @@ export async function loadAutoImports() {
295
322
  try {
296
323
  const module = await import(file)
297
324
  for (const [name, value] of Object.entries(module)) {
298
- // Skip default exports and protected globals
299
- if (name === 'default' || protectedGlobals.has(name)) continue
325
+ // Skip default exports and anything the runtime already owns
326
+ if (name === 'default' || isProtectedGlobal(name)) continue
300
327
  if (typeof value !== 'undefined') {
301
328
  Reflect.set(globalThis, name, value)
302
329
  }
@@ -326,7 +353,7 @@ export async function loadAutoImports() {
326
353
  if (file.endsWith('.d.ts') || file.endsWith('/index.ts')) continue
327
354
 
328
355
  const modelName = file.split('/').pop()?.replace('.ts', '') || ''
329
- if (!modelName || loadedModels.has(modelName) || protectedGlobals.has(modelName)) continue
356
+ if (!modelName || loadedModels.has(modelName) || isProtectedGlobal(modelName)) continue
330
357
 
331
358
  try {
332
359
  const module = await import(file)
@@ -357,7 +384,7 @@ export async function loadAutoImports() {
357
384
  if (file.endsWith('.d.ts') || file.endsWith('/index.ts')) continue
358
385
 
359
386
  const jobName = file.split('/').pop()?.replace('.ts', '') || ''
360
- if (!jobName || loadedJobs.has(jobName) || protectedGlobals.has(jobName)) continue
387
+ if (!jobName || loadedJobs.has(jobName) || isProtectedGlobal(jobName)) continue
361
388
 
362
389
  try {
363
390
  const module = await import(file)
@@ -392,7 +419,7 @@ export async function loadAutoImports() {
392
419
  if (file.endsWith('.d.ts') || file.endsWith('/index.ts')) continue
393
420
 
394
421
  const controllerName = file.split('/').pop()?.replace('.ts', '') || ''
395
- if (!controllerName || loadedControllers.has(controllerName) || protectedGlobals.has(controllerName)) continue
422
+ if (!controllerName || loadedControllers.has(controllerName) || isProtectedGlobal(controllerName)) continue
396
423
 
397
424
  try {
398
425
  const module = await import(file)
@@ -8,10 +8,19 @@
8
8
  */
9
9
  import { isLocalDeployment } from '@stacksjs/env'
10
10
 
11
+ /*
12
+ * Handed in by TestErrorAction's renderTemplate call, which spreads its
13
+ * `context` into the template scope - there is no `context` variable here.
14
+ * Undefined when the view fallback serves this file on its own. Read into a
15
+ * differently named list: a local `const scenarios = ...` shadows the injected
16
+ * one and the page lists nothing.
17
+ */
18
+ declare const scenarios: Array<{ title: string, description: string, status: number, href: string }> | undefined
19
+
11
20
  if (!isLocalDeployment())
12
21
  notFound()
13
22
 
14
- const scenarios = typeof context !== 'undefined' && Array.isArray(context?.scenarios) ? context.scenarios : []
23
+ const scenarioList = typeof scenarios !== 'undefined' && Array.isArray(scenarios) ? scenarios : []
15
24
  </script>
16
25
 
17
26
  <div class="mx-auto px-6 py-12 max-w-3xl">
@@ -21,7 +30,7 @@ const scenarios = typeof context !== 'undefined' && Array.isArray(context?.scena
21
30
  </p>
22
31
 
23
32
  <div class="grid gap-3">
24
- @foreach(scenarios as scenario)
33
+ @foreach(scenarioList as scenario)
25
34
  <a
26
35
  href="{{ scenario.href }}"
27
36
  class="block p-4 bg-white dark:bg-white/[2%] border border-neutral-200 rounded-lg dark:border-white/10 hover:border-rose-500 transition-colors"