@stacksjs/defaults 0.75.41 → 0.75.42
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.
- package/ai/skills/stacks-actions/SKILL.md +1 -1
- package/ai/skills/stacks-auto-imports/SKILL.md +1 -1
- package/ai/skills/stacks-chat/SKILL.md +43 -1
- package/ai/skills/stacks-cloud/SKILL.md +36 -0
- package/ai/skills/stacks-dashboard/SKILL.md +16 -2
- package/ai/skills/stacks-database/SKILL.md +24 -3
- package/ai/skills/stacks-deploy/SKILL.md +7 -1
- package/ai/skills/stacks-desktop/SKILL.md +10 -0
- package/ai/skills/stacks-development/SKILL.md +1 -1
- package/ai/skills/stacks-migrations/SKILL.md +2 -2
- package/ai/skills/stacks-models/SKILL.md +38 -2
- package/ai/skills/stacks-orm/SKILL.md +2 -2
- package/ai/skills/stacks-security/SKILL.md +28 -1
- package/app/Actions/Auth/DataExportAction.test.ts +84 -0
- package/app/Actions/Auth/DataExportAction.ts +40 -0
- package/app/Actions/Dashboard/Content/FileReprocessAction.ts +1 -1
- package/app/Actions/Dashboard/Content/file-manager.ts +120 -8
- package/app/Actions/Dashboard/Content/file-metadata.ts +108 -12
- package/app/Actions/Dashboard/Content/file-pipeline.test.ts +13 -9
- package/app/Actions/Dashboard/Content/file-preview.test.ts +487 -0
- package/app/Actions/Dashboard/Content/file-preview.ts +548 -0
- package/app/Jobs/GenerateStoragePreviewJob.ts +47 -0
- package/app/Jobs/OptimizeStorageImageJob.ts +7 -3
- package/app/Jobs/PruneRetainedDataJob.ts +46 -0
- package/app/Models/Activity.ts +2 -0
- package/app/Models/CampaignSend.ts +2 -0
- package/app/Models/Card.ts +1 -0
- package/app/Models/CardComment.ts +1 -0
- package/app/Models/Comment.ts +5 -0
- package/app/Models/CommunicationSuppression.ts +2 -1
- package/app/Models/ConsentEvent.ts +4 -3
- package/app/Models/Content/Author.ts +5 -0
- package/app/Models/EmailListSubscriber.ts +1 -0
- package/app/Models/EmailSuppression.ts +2 -0
- package/app/Models/Forms/FormSubmission.ts +5 -0
- package/app/Models/GdprRequest.ts +77 -0
- package/app/Models/MagicLinkToken.ts +2 -0
- package/app/Models/Notification.ts +2 -0
- package/app/Models/NotificationDelivery.ts +4 -0
- package/app/Models/PaymentMethod.ts +5 -0
- package/app/Models/PaymentTransaction.ts +1 -0
- package/app/Models/PersonalAccessToken.ts +3 -0
- package/app/Models/Referral.ts +4 -1
- package/app/Models/ReferralCode.ts +4 -1
- package/app/Models/Request.ts +3 -0
- package/app/Models/SmsOptOut.ts +2 -0
- package/app/Models/SocialAccount.ts +3 -0
- package/app/Models/SocialPost.ts +2 -0
- package/app/Models/StorageItemTask.ts +14 -8
- package/app/Models/Subscriber.ts +2 -0
- package/app/Models/SubscriberEmail.ts +2 -0
- package/app/Models/Subscription.ts +1 -0
- package/app/Models/TeamInvitation.ts +2 -0
- package/app/Models/TeamMember.ts +1 -0
- package/app/Models/User.ts +5 -0
- package/app/Models/commerce/Bid.ts +3 -0
- package/app/Models/commerce/Cart.ts +2 -0
- package/app/Models/commerce/Courier.ts +7 -0
- package/app/Models/commerce/CourierPing.ts +3 -0
- package/app/Models/commerce/Customer.ts +5 -0
- package/app/Models/commerce/DeliveryStop.ts +7 -0
- package/app/Models/commerce/GiftCard.ts +4 -0
- package/app/Models/commerce/Order.ts +5 -0
- package/app/Models/commerce/OrderItem.ts +2 -0
- package/app/Models/commerce/Payment.ts +5 -0
- package/app/Models/commerce/Pledge.ts +3 -0
- package/app/Models/commerce/Transaction.ts +2 -0
- package/app/Models/commerce/WaitlistProduct.ts +5 -0
- package/app/Models/commerce/WaitlistRestaurant.ts +4 -0
- package/ide/vscode/.vscode/extensions.json +1 -1
- package/ide/vscode/.vscode/settings.json +8 -149
- package/ide/vscode/README.md +48 -23
- package/ide/vscode/build.ts +43 -0
- package/ide/vscode/package.json +305 -56
- package/ide/vscode/scripts/stx-contributes.ts +66 -0
- package/ide/vscode/src/env-file.ts +103 -0
- package/ide/vscode/src/env.ts +241 -0
- package/ide/vscode/src/extension.ts +120 -26
- package/ide/vscode/src/pickier-protocol.ts +51 -0
- package/ide/vscode/src/pickier-worker.ts +92 -0
- package/ide/vscode/src/pickier.ts +433 -0
- package/ide/vscode/src/project.ts +14 -4
- package/ide/vscode/src/stacks.ts +18 -11
- package/ide/vscode/tests/build.test.ts +58 -0
- package/ide/vscode/tests/env.test.ts +166 -0
- package/ide/vscode/tests/extension.test.ts +8 -0
- package/ide/vscode/tests/fake-vscode.ts +228 -0
- package/ide/vscode/tests/manifest.test.ts +88 -0
- package/ide/vscode/tests/pickier.test.ts +202 -0
- package/ide/vscode/tsconfig.json +2 -2
- package/package.json +2 -2
- package/resources/components/Dashboard/Content/FileManagerDashboard.stx +2 -0
- package/resources/components/Dashboard/FileViewer/MediaCard.stx +14 -3
- package/routes/auth.ts +4 -0
- package/vcs/github/workflows/buddy-bot.yml +2 -2
- package/vcs/github/workflows/ci.yml +3 -3
- package/vcs/github/workflows/release.yml +1 -1
- package/ide/vscode/src/vscode.d.ts +0 -181
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stacks-actions
|
|
3
|
-
description: Use when working with Stacks server actions - creating actions in app/Actions/, auto-generated API actions from the useApi model trait, the
|
|
3
|
+
description: Use when working with Stacks server actions - creating actions in app/Actions/, auto-generated API actions from the useApi model trait, the 650 default framework actions (auth, dashboard, commerce, content, deployment, jobs), action request/response handling, or action registration. Covers @stacksjs/actions and storage/framework/defaults/app/Actions/.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Bun >= 1.3.0, TypeScript
|
|
6
6
|
allowed-tools: Read Edit Write Bash Grep Glob
|
|
@@ -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. (
|
|
69
|
+
- **All ORM Models**: User, Post, Author, Product, Order, Payment, Customer, etc. (104 models)
|
|
70
70
|
- **Request Models**: UserRequest, PostRequest, OrderRequest, etc.
|
|
71
71
|
- **Actions**: Action types and helpers
|
|
72
72
|
- **Schema**: validation schema builder
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stacks-chat
|
|
3
|
-
description: Use when implementing chat messaging in Stacks - sending messages to Slack (webhooks, bot tokens, block kit), Discord (webhooks, bot tokens, embeds), Microsoft Teams (adaptive cards, webhooks), the BaseChatDriver abstraction, retry logic,
|
|
3
|
+
description: Use when implementing chat messaging in Stacks - sending messages to Slack (webhooks, bot tokens, block kit), Discord (webhooks, bot tokens, embeds), Microsoft Teams (adaptive cards, webhooks), the BaseChatDriver abstraction, retry logic, multi-channel chat routing, or READING a person's conversations from iMessage, Slack and Discord and archiving them there (the inbox drivers). Covers @stacksjs/chat.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Bun >= 1.3.0, TypeScript
|
|
6
6
|
allowed-tools: Read Edit Write Bash Grep Glob
|
|
@@ -342,6 +342,48 @@ async function sendCard(webhookUrl: string, card: TeamsAdaptiveCard, summary?: s
|
|
|
342
342
|
- `driver` -- pre-instantiated singleton
|
|
343
343
|
- `send()`, `sendWebhook()`, `sendCard()`, `configure()` functions
|
|
344
344
|
|
|
345
|
+
## Inbox drivers (reading and archiving)
|
|
346
|
+
|
|
347
|
+
`send()` pushes messages out. The inbox drivers go the other way: they list a
|
|
348
|
+
person's conversations, read messages, notice new ones (a per-conversation
|
|
349
|
+
`cursor`), and archive a conversation *in the real app* as far as the provider
|
|
350
|
+
allows. Source: `chat/src/inbox/`. One interface, `InboxDriver`:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { inbox } from '@stacksjs/chat'
|
|
354
|
+
|
|
355
|
+
const drivers = [
|
|
356
|
+
inbox.createInboxDriver('imessage', {}), // chat.db, needs Full Disk Access
|
|
357
|
+
inbox.createInboxDriver('slack', { token: env.SLACK_USER_TOKEN }), // xoxp user token
|
|
358
|
+
inbox.createInboxDriver('discord', { token: env.DISCORD_BOT_TOKEN, userId }), // bot token
|
|
359
|
+
]
|
|
360
|
+
for (const driver of drivers) {
|
|
361
|
+
if (!(await driver.status()).connected) continue
|
|
362
|
+
for (const c of await driver.conversations()) {
|
|
363
|
+
const fresh = await driver.messages(c.id, { after: lastCursor[c.id] }) // oldest first
|
|
364
|
+
if (wantArchived) await driver.archive(c.id)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
What archiving does is part of every conversation (`conversation.archive`):
|
|
370
|
+
|
|
371
|
+
| Provider | `mode` | Effect in the real app |
|
|
372
|
+
|---|---|---|
|
|
373
|
+
| iMessage | `confirm` | Opens the conversation in Messages at Delete Conversation; the person confirms. Apple has no API, and editing chat.db is ignored or synced everywhere. |
|
|
374
|
+
| Slack DM / group DM | `native` | `conversations.close`; Slack reopens it when someone writes. `unarchive` reopens. |
|
|
375
|
+
| Slack public channel | `native` | `conversations.leave`; `unarchive` rejoins. |
|
|
376
|
+
| Slack private channel | `unsupported` | Untouched: leaving would need a re-invite. |
|
|
377
|
+
| Discord thread | `native` | Archived in Discord (reopens on a new post). |
|
|
378
|
+
| Discord channel | `unsupported` | Discord cannot hide a channel for one person. |
|
|
379
|
+
|
|
380
|
+
Discord personal DMs are deliberately absent: only a user token reads them, and
|
|
381
|
+
automating a user account breaks Discord's terms. Reactions are separate
|
|
382
|
+
`kind: 'reaction'` messages with `targetId` and an emoji `reaction`; Slack and
|
|
383
|
+
Discord attachments carry a remote `url` that `driver.attachment()` fetches with
|
|
384
|
+
the right credentials. The HTTP drivers take an injectable `fetch`, which is how
|
|
385
|
+
`tests/inbox.test.ts` exercises them against API-shaped fixtures.
|
|
386
|
+
|
|
345
387
|
## Retry Logic
|
|
346
388
|
|
|
347
389
|
All three drivers implement `sendWithRetry()`:
|
|
@@ -68,6 +68,40 @@ buddy deploy # deploy to cloud
|
|
|
68
68
|
- Tags: Environment, Project, ManagedBy
|
|
69
69
|
- OnFailure: ROLLBACK
|
|
70
70
|
|
|
71
|
+
## CloudFront origin failover (AWS)
|
|
72
|
+
|
|
73
|
+
Off unless configured. Two shapes, both under `tsCloud.infrastructure` in `config/cloud.ts`, both
|
|
74
|
+
generated by ts-cloud into a CloudFront origin group (primary + secondary, failover status codes):
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
storage: {
|
|
78
|
+
public: {
|
|
79
|
+
website: true,
|
|
80
|
+
// Replica of this bucket in another region, replicated with S3 replication.
|
|
81
|
+
failover: { region: 'us-west-2', connectionAttempts: 1, connectionTimeout: 3 },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
cdn: {
|
|
85
|
+
// A standalone distribution: any second origin, string or { domain, originPath, connection* }.
|
|
86
|
+
frontend: { origin: 'a.s3.us-east-1.amazonaws.com', failoverOrigin: 'b.s3.us-west-2.amazonaws.com' },
|
|
87
|
+
},
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- Bucket `failover` needs CloudFront in front of the bucket: a website bucket, `dns.domain`, and SSL
|
|
91
|
+
(`ssl.certificateArn`, or `ssl.enabled` + `dns.hostedZoneId`). Without them generation throws.
|
|
92
|
+
- Default codes: bucket failover `[403, 404, 500, 502, 503, 504]` (OAC answers a missing object
|
|
93
|
+
with 403); `cdn` failover `[500, 502, 503, 504]`. CloudFront accepts 400, 403, 404, 416, 429,
|
|
94
|
+
500, 502, 503, 504 and nothing else.
|
|
95
|
+
- Only GET/HEAD/OPTIONS behaviors may target the group. Compute routes (`/api/*`, which accept
|
|
96
|
+
writes) stay on their own origin; a group behind a write-capable behavior throws at generation.
|
|
97
|
+
- `connectionAttempts` (1-3) / `connectionTimeout` (1-10 s) on the primary decide how fast an
|
|
98
|
+
unreachable primary fails over. CloudFront's default is 3 x 10 s.
|
|
99
|
+
- The replica is created by the deploy, not the stack (a stack only makes buckets in its own
|
|
100
|
+
region). Template metadata `TsCloud::StorageFailover` lists replicas; the deploy runs
|
|
101
|
+
`ensureFailoverReplicaBuckets` before the stack, `grantFailoverReplicaAccess` and
|
|
102
|
+
`seedFailoverReplicas` after (all from `@stacksjs/ts-cloud`).
|
|
103
|
+
- Docs: `docs/guide/cloud/origin-failover.md`.
|
|
104
|
+
|
|
71
105
|
## Hetzner and SSH targets
|
|
72
106
|
|
|
73
107
|
Both deploy the same way once a host exists: build each site, package a release tarball, copy it
|
|
@@ -256,6 +290,8 @@ Stack naming: `{slugified-app-name}-cloud`
|
|
|
256
290
|
- Default region is `us-east-1` (from AWS_DEFAULT_REGION env)
|
|
257
291
|
- APP_KEY must be colon-separated format (validated during deployment)
|
|
258
292
|
- Server mode uses EC2 + ALB; serverless uses Lambda + API Gateway + CloudFront
|
|
293
|
+
- Origin failover only retries GET, HEAD and OPTIONS on a cache miss; it does not protect writes,
|
|
294
|
+
and the failover replica outlives `buddy cloud:remove`
|
|
259
295
|
- Jump boxes are optional, used for SSH access to private instances
|
|
260
296
|
- `cloud:remove` with `--force` skips confirmation and is destructive
|
|
261
297
|
- CDK toolkit stack is named `stacks-toolkit`
|
|
@@ -92,8 +92,8 @@ transcode is minutes, a vision call is a round trip to a third party. So an
|
|
|
92
92
|
upload dispatches and the dashboard shows state.
|
|
93
93
|
|
|
94
94
|
- `storage_item_tasks`, one row per `(disk, path, kind)`, kind being
|
|
95
|
-
`optimize` (images, via `ts-images`), `transcode` (video, via `ts-videos`)
|
|
96
|
-
`tag` (a vision model). They succeed and fail independently, which is why this
|
|
95
|
+
`optimize` (images, via `ts-images`), `transcode` (video, via `ts-videos`),
|
|
96
|
+
`tag` (a vision model) or `preview` (the file manager thumbnail, #308). They succeed and fail independently, which is why this
|
|
97
97
|
is not a column on `storage_items` - a video whose transcode finished and
|
|
98
98
|
whose tagging failed is a normal state.
|
|
99
99
|
- `dispatchDashboardFileTasks` decides from the CONTENT TYPE what a file needs.
|
|
@@ -107,10 +107,24 @@ upload dispatches and the dashboard shows state.
|
|
|
107
107
|
and the queue answer different questions - the queue decides whether to retry,
|
|
108
108
|
the row is what somebody looking at the file sees.
|
|
109
109
|
- `POST /files/reprocess` re-runs everything, or the kinds you name.
|
|
110
|
+
- `preview` is decided by `file-preview.ts`, from the type the file CLAIMS
|
|
111
|
+
(the disk's answer, or the extension when that is `octet-stream`), and the
|
|
112
|
+
job checks the bytes agree before drawing. Images, WAV waveforms and TrueType
|
|
113
|
+
specimens become `.variants/<path>/preview.png`; text becomes a
|
|
114
|
+
`preview.txt` snippet. SVG, video, compressed audio, PDF, CFF/WOFF fonts,
|
|
115
|
+
HEIC and TIFF are recorded `skipped` with the reason in `error` - no
|
|
116
|
+
homegrown renderer draws them, and SVG is never rasterized. Never add a
|
|
117
|
+
renderer that shells out: that is the rule the skips exist to keep.
|
|
118
|
+
- `skipped` is terminal: `runTask` records a thrown `StorageTaskSkipped`
|
|
119
|
+
without rethrowing, so the queue does not retry it, and `aggregateTaskState`
|
|
120
|
+
ignores it unless nothing else applied.
|
|
110
121
|
|
|
111
122
|
Derivatives are written back to the same disk under `.variants/<path>/`. The
|
|
112
123
|
leading dot keeps them out of the listing, which skips hidden components - a
|
|
113
124
|
folder of thirty derivatives beside every photo makes the browser useless.
|
|
125
|
+
A rename or delete through the dashboard moves or removes that folder with the
|
|
126
|
+
file, after the storage operation succeeds and best effort, since the task rows
|
|
127
|
+
already followed and a missing derivative is regenerated by reprocessing.
|
|
114
128
|
|
|
115
129
|
### Remote commands (stacksjs/stacks#960)
|
|
116
130
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stacks-database
|
|
3
|
-
description: Use when working with databases in a Stacks application - configuring connections, running queries, migrations, seeding, SQL helpers, or using SQLite/MySQL/PostgreSQL/DynamoDB. Covers @stacksjs/database, bun-query-builder, config/database.ts, and the database/ migrations directory.
|
|
3
|
+
description: Use when working with databases in a Stacks application - configuring connections, running queries, migrations, seeding, SQL helpers, or using SQLite/Turso/MySQL/PostgreSQL/DynamoDB. Covers @stacksjs/database, bun-query-builder, config/database.ts, and the database/ migrations directory.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Bun >= 1.3.0, TypeScript, SQLite >= 3.47.2
|
|
6
6
|
allowed-tools: Read Edit Write Bash Grep Glob
|
|
@@ -12,7 +12,7 @@ allowed-tools: Read Edit Write Bash Grep Glob
|
|
|
12
12
|
- Database package: `storage/framework/core/database/src/`
|
|
13
13
|
- Configuration: `config/database.ts`
|
|
14
14
|
- QB config: `config/query-builder.ts`
|
|
15
|
-
- Migrations: `database/migrations/` (
|
|
15
|
+
- Migrations: `database/migrations/` (233 migration files, `.sql` format)
|
|
16
16
|
- QB state: `.qb/`
|
|
17
17
|
- ORM: `storage/framework/orm/`
|
|
18
18
|
|
|
@@ -60,6 +60,7 @@ Database.fromEnv() // From env vars (DB_CONNECTION, DB_DATABASE
|
|
|
60
60
|
## Factory Functions (database.ts)
|
|
61
61
|
- `createDatabase(options: DatabaseOptions): Database`
|
|
62
62
|
- `createSqliteDatabase(database: string, options?): Database`
|
|
63
|
+
- `createTursoDatabase(url: string, authToken?: string, options?): Database`
|
|
63
64
|
- `createPostgresDatabase(connection: DatabaseConnectionConfig, options?): Database`
|
|
64
65
|
- `createMysqlDatabase(connection: DatabaseConnectionConfig, options?): Database`
|
|
65
66
|
|
|
@@ -154,6 +155,7 @@ interface DatabaseOptions {
|
|
|
154
155
|
|
|
155
156
|
```typescript
|
|
156
157
|
interface SqliteConfig { database: string, prefix?: string }
|
|
158
|
+
interface TursoConfig { url: string, authToken?: string, prefix?: string }
|
|
157
159
|
interface MysqlConfig { name: string, host?: string, port?: number, username?: string, password?: string, prefix?: string, charset?: string, collation?: string }
|
|
158
160
|
interface PostgresConfig { name: string, host?: string, port?: number, username?: string, password?: string, prefix?: string, schema?: string, sslMode?: 'disable' | 'require' | 'verify-ca' | 'verify-full' }
|
|
159
161
|
interface DynamoDbConfig { key: string, secret: string, region?: string, prefix?: string, endpoint?: string, tableName?: string, singleTable?: { enabled?, pkAttribute?, skAttribute?, entityTypeAttribute?, keyDelimiter?, gsiCount? } }
|
|
@@ -256,11 +258,30 @@ Entity-centric API for single-table design:
|
|
|
256
258
|
- `buddy seed` -- seed database
|
|
257
259
|
- `buddy generate:migrations` -- generate migration diffs from models
|
|
258
260
|
|
|
261
|
+
## Turso / libSQL (`DB_CONNECTION=turso`, alias `libsql`)
|
|
262
|
+
|
|
263
|
+
SQLite's SQL on a Turso or `sqld` server. Configure `TURSO_DATABASE_URL`
|
|
264
|
+
(`libsql://<db>-<org>.turso.io`, or `http://127.0.0.1:8080` for `turso dev`) and
|
|
265
|
+
`TURSO_AUTH_TOKEN` (a secret, never logged).
|
|
266
|
+
|
|
267
|
+
- Everything that renders SQL or DDL treats it as `sqlite`: `dialectCapabilities('turso').wire === 'sqlite'`,
|
|
268
|
+
`getDatabaseDialect()` returns `'sqlite'`, and it shares the SQLite migration corpus and
|
|
269
|
+
`model-snapshot.sqlite.json`. Switching between `sqlite` and `turso` needs no regeneration.
|
|
270
|
+
- The transport is bun-query-builder's: `setConfig({ dialect: 'sqlite', database: { url, authToken } })`
|
|
271
|
+
with a libSQL URL selects its dependency-free Hrana-over-HTTP connection (`createLibSQLSQL`).
|
|
272
|
+
- Code that opens the database FILE must ask `isLibsqlDriver(driver)` first - there is none. The SQLite
|
|
273
|
+
migration preprocessor reads an in-memory replica of the server's schema instead
|
|
274
|
+
(`loadLibsqlSchemaMirror` in migrations.ts), and the migration lock is a row in
|
|
275
|
+
`stacks_migration_lock` (`acquireLibsqlMigrationLock`), since migrating hosts share no filesystem.
|
|
276
|
+
- libSQL rejects SQLite's double-quoted-string fallback, so a statement naming a missing column fails
|
|
277
|
+
instead of storing the column name as text. `tests/corpus-rebuild-sources.test.ts` guards the corpus.
|
|
278
|
+
- Inside `transaction()`, write through the `tx` handle; model writes use their own connection.
|
|
279
|
+
|
|
259
280
|
## config/database.ts Shape
|
|
260
281
|
```typescript
|
|
261
282
|
{
|
|
262
283
|
default: env.DB_CONNECTION || 'mysql',
|
|
263
|
-
connections: { sqlite, mysql, postgres, dynamodb },
|
|
284
|
+
connections: { sqlite, turso, mysql, singlestore, vitess, postgres, dynamodb },
|
|
264
285
|
migrations: 'migrations',
|
|
265
286
|
migrationLocks: 'migration_locks',
|
|
266
287
|
queryLogging: {
|
|
@@ -75,10 +75,14 @@ a versioned `STACKS_DEPLOY_PREVIEW_JSON=` line for API and tool consumers.
|
|
|
75
75
|
1. **Validation**: checks APP_KEY format, AWS region, app URL, team config
|
|
76
76
|
2. **Build**: compiles application for production
|
|
77
77
|
3. **Infrastructure**: generates CloudFormation template via ts-cloud
|
|
78
|
-
4. **
|
|
78
|
+
4. **Failover replicas** (only when a bucket sets `failover`): creates each replica bucket in its
|
|
79
|
+
region before the stack, since the stack cannot and its replication rule needs it
|
|
80
|
+
5. **Deploy**: creates or updates the CloudFormation stack
|
|
79
81
|
- Capabilities: CAPABILITY_IAM, CAPABILITY_NAMED_IAM
|
|
80
82
|
- OnFailure: ROLLBACK
|
|
81
83
|
- Tags: Environment, Project, ManagedBy
|
|
84
|
+
6. **Failover access**: lets the new distribution read each replica, then copies across objects
|
|
85
|
+
that predate replication
|
|
82
86
|
|
|
83
87
|
**Hetzner and SSH**
|
|
84
88
|
|
|
@@ -190,4 +194,6 @@ buddy cloud:cleanup # clean retained resources
|
|
|
190
194
|
fragment serves
|
|
191
195
|
- `.env.<environment>` is decrypted at deploy time and shipped as every site's `.env`, so declare
|
|
192
196
|
`tenants` in `config/cloud.ts` on a shared box to strip other tenants' keys
|
|
197
|
+
- A failover replica bucket (`infrastructure.storage.<name>.failover`) lives outside the stack, so
|
|
198
|
+
`buddy cloud:remove` leaves it, and its data, behind on purpose
|
|
193
199
|
- For infrastructure details, see the `stacks-cloud` skill
|
|
@@ -240,6 +240,15 @@ uses App Store Connect API-key authentication for validation/upload.
|
|
|
240
240
|
Start with `validate-only: true`. Upload only after the signed artifact passes
|
|
241
241
|
local launch and UI QA.
|
|
242
242
|
|
|
243
|
+
To package on a developer machine without importing distribution keys into the
|
|
244
|
+
login keychain, generate keys and certificates with `buddy desktop:apple:csr`
|
|
245
|
+
and `buddy desktop:apple:provision --apply`, import the two `.p12` files into a
|
|
246
|
+
throwaway keychain file, and point `--keychain <path>` (or `APPLE_KEYCHAIN`) at
|
|
247
|
+
it. `codesign` and `productbuild` then look for identities in that file only,
|
|
248
|
+
so the search list never changes. The Mac App Distribution identity is named
|
|
249
|
+
`3rd Party Mac Developer Application: ...` and the installer one
|
|
250
|
+
`3rd Party Mac Developer Installer: ...`.
|
|
251
|
+
|
|
243
252
|
Human-owned prerequisites that Buddy does not pretend to automate:
|
|
244
253
|
|
|
245
254
|
- Apple Developer Program enrollment and agreement acceptance
|
|
@@ -257,6 +266,7 @@ Repository variables:
|
|
|
257
266
|
- `DESKTOP_URL`
|
|
258
267
|
- `APPLE_APP_SIGNING_IDENTITY`
|
|
259
268
|
- `APPLE_INSTALLER_SIGNING_IDENTITY`
|
|
269
|
+
- `APPLE_KEYCHAIN` (local only, optional): sign from this keychain file instead of the search list
|
|
260
270
|
|
|
261
271
|
Repository secrets:
|
|
262
272
|
|
|
@@ -252,7 +252,7 @@ Default configurations provided at `storage/framework/defaults/ide/`:
|
|
|
252
252
|
- `vscode/.vscode/extensions.json` -- Recommended extensions
|
|
253
253
|
- `vscode/.vscode/settings.json` -- Editor settings
|
|
254
254
|
- `vscode/.vscode/stacks.code-snippets` -- Code snippets
|
|
255
|
-
- `vscode/package.json` --
|
|
255
|
+
- `vscode/package.json` -- the Stacks extension (`Stacks.vscode-stacks`). Self-contained: it installs no extension pack and depends on no extension. It builds in `.stx` support from `@stacksjs/stx-vscode` (the stx extension's language support as a library; the standalone stx extension, `Stacks.vscode-stx`, stands down next to it), pickier lint/format/fix-all through the project's own pickier, and env file hover decryption and `buddy env:*` commands. Run `bun run sync:stx` there after updating `@stacksjs/stx-vscode`; never map `*.stx` to `html` in `files.associations`, which takes `.stx` files away from the stx language.
|
|
256
256
|
|
|
257
257
|
### JetBrains
|
|
258
258
|
- `.idea/` config (ESLint, modules, VCS, web resources)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stacks-migrations
|
|
3
|
-
description: Use when working with database migrations in a Stacks application - creating migration files, running migrations, fresh migration (drop + recreate), seeding after migration, migration file naming conventions, or the
|
|
3
|
+
description: Use when working with database migrations in a Stacks application - creating migration files, running migrations, fresh migration (drop + recreate), seeding after migration, migration file naming conventions, or the 233 built-in migration files. For the database API itself (queries, connections, SQL helpers), see stacks-database.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Bun >= 1.3.0, TypeScript, SQLite >= 3.47.2
|
|
6
6
|
allowed-tools: Read Edit Write Bash Grep Glob
|
|
@@ -119,6 +119,6 @@ The framework includes migrations for all built-in models:
|
|
|
119
119
|
- Do not commit a second snapshot under `.qb/`; that indicates a missing `snapshotDir` configuration
|
|
120
120
|
- If a generated SQLite migration rebuilds tables, test it against a copy of the current database and run `PRAGMA integrity_check` plus `PRAGMA foreign_key_check`
|
|
121
121
|
- `--seed` flag after `migrate:fresh` seeds the database with factory data
|
|
122
|
-
-
|
|
122
|
+
- 233 migration files exist by default for all framework models
|
|
123
123
|
- SQLite >= 3.47.2 is required (system requirement)
|
|
124
124
|
- For the database API (queries, connections), see the `stacks-database` skill
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stacks-models
|
|
3
|
-
description: Use when working with data models in Stacks - the defineModel() API, model attributes with validation and factories, relationships (hasOne/hasMany/belongsTo/belongsToMany), traits (useAuth, useUuid, useTimestamps, useSearch, useApi, billable, taggable, categorizable, commentable, likeable, observe), computed properties (get/set), model generation, and the
|
|
3
|
+
description: Use when working with data models in Stacks - the defineModel() API, model attributes with validation and factories, relationships (hasOne/hasMany/belongsTo/belongsToMany), traits (useAuth, useUuid, useTimestamps, useSearch, useApi, billable, taggable, categorizable, commentable, likeable, observe), computed properties (get/set), model generation, and the 104 built-in framework models. Covers model definitions and storage/framework/defaults/app/Models/.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Bun >= 1.3.0, TypeScript, SQLite >= 3.47.2
|
|
6
6
|
allowed-tools: Read Edit Write Bash Grep Glob
|
|
@@ -88,6 +88,7 @@ generated model types stay precise.
|
|
|
88
88
|
| `guarded` | Block mass assignment |
|
|
89
89
|
| `hidden` | Exclude from JSON serialization (passwords, tokens) |
|
|
90
90
|
| `foreignKey` | Disable, infer, or configure the FK constraint |
|
|
91
|
+
| `personal` | Personal data: exported by `gdpr:export`, anonymized by erasure and retention. `true`, or `{ anonymize?, export? }`. See "Personal data (GDPR)" below |
|
|
91
92
|
| `factory` | `(faker, attributes) => value`, used by seeders and tests. `attributes` holds what this record's earlier-declared factories produced, so one value can depend on another: `(faker, { discountType }) => ...` |
|
|
92
93
|
| `validation` | `{ rule, message? }` - `rule` from `schema`, `message` keyed by rule name |
|
|
93
94
|
|
|
@@ -107,6 +108,7 @@ generated model types stay precise.
|
|
|
107
108
|
| `observe` | Emits `{model}:created` / `:updated` / `:deleted` events |
|
|
108
109
|
| `billable` | Stripe methods (`checkout()`, `activeSubscription()`, ...) |
|
|
109
110
|
| `taggable` / `categorizable` / `commentable` / `likeable` | Pivot tables and their relation methods |
|
|
111
|
+
| `gdpr` | Whose data the rows are (`subject`), what erasure does (`erasure`), how long rows live (`retention`), and why (`basis`, `purpose`). See "Personal data (GDPR)" below |
|
|
110
112
|
|
|
111
113
|
Also at the top level: `indexes: [{ name, columns, unique?, where? }]` for
|
|
112
114
|
composite and partial-unique indexes, and `dashboard: { highlight: true }` to
|
|
@@ -212,6 +214,40 @@ scopes: {
|
|
|
212
214
|
},
|
|
213
215
|
```
|
|
214
216
|
|
|
217
|
+
## Personal data (GDPR)
|
|
218
|
+
|
|
219
|
+
Declared on the model, read by access exports, erasure, retention and the
|
|
220
|
+
processing register (stacksjs/stacks#365). Full guide: `docs/guide/gdpr.md`.
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
belongsTo: ['Customer'],
|
|
224
|
+
traits: {
|
|
225
|
+
gdpr: {
|
|
226
|
+
subject: { via: 'Customer' }, // or 'user_id', ['a_id', 'b_id'], { column, where }, { email: 'col' }
|
|
227
|
+
erasure: 'anonymize', // default; or 'delete' | 'keep'
|
|
228
|
+
retention: { days: 3650, action: 'anonymize' }, // optional; column defaults to created_at
|
|
229
|
+
basis: 'legal_obligation',
|
|
230
|
+
purpose: 'Order fulfilment, kept for tax and accounting',
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
attributes: {
|
|
234
|
+
deliveryAddress: { personal: true, validation: { rule: schema.string() } },
|
|
235
|
+
},
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
- `subject` is derived when left out: `id` on `User`, the `belongsTo: ['User']`
|
|
239
|
+
foreign key elsewhere. A `via` parent must itself declare a subject.
|
|
240
|
+
- Anonymization writes NULL to nullable columns and a typed placeholder to
|
|
241
|
+
`NOT NULL` ones (`erased-<id>` when unique). A `NOT NULL` enum or timestamp
|
|
242
|
+
needs `personal: { anonymize: <value> }`, or every request refuses to run.
|
|
243
|
+
- A model that `belongsTo: ['User']` and declares nothing is **unclassified**:
|
|
244
|
+
erasure does not reach it and `buddy gdpr:register` lists it. Classify it, even
|
|
245
|
+
as `erasure: 'keep'`. `core/orm/tests/gdpr.test.ts` fails if a built-in one is.
|
|
246
|
+
- Engine: `exportSubjectData`, `eraseSubject({ dryRun })`, `pruneRetainedData`,
|
|
247
|
+
`resolveGdprPlan`, `renderProcessingRegister`, all from `@stacksjs/orm`.
|
|
248
|
+
Commands: `buddy gdpr:export|erase|prune|register|register:check`. Every
|
|
249
|
+
request writes a `gdpr_requests` row (the `GdprRequest` model).
|
|
250
|
+
|
|
215
251
|
## Workflow
|
|
216
252
|
|
|
217
253
|
```sh
|
|
@@ -265,7 +301,7 @@ pass `--allow-protected` to override.
|
|
|
265
301
|
## Built-in models by category
|
|
266
302
|
|
|
267
303
|
The ones below are worth knowing by name. They are a selection, not the set:
|
|
268
|
-
`storage/framework/defaults/app/Models/` holds
|
|
304
|
+
`storage/framework/defaults/app/Models/` holds 104, and that directory is the
|
|
269
305
|
authority. This section said "All 62 built-in models by category" while
|
|
270
306
|
listing fewer than that against 102 on disk, so an agent reading to the end
|
|
271
307
|
had no way to tell it was short.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stacks-orm
|
|
3
|
-
description: Use when working with the Stacks ORM - defining models with defineModel(), model relationships (hasOne, hasMany, belongsTo, belongsToMany, morphOne, hasManyThrough), attributes, traits, factories, computed properties, query building, transactions, or the
|
|
3
|
+
description: Use when working with the Stacks ORM - defining models with defineModel(), model relationships (hasOne, hasMany, belongsTo, belongsToMany, morphOne, hasManyThrough), attributes, traits, factories, computed properties, query building, transactions, or the 104 built-in models. Covers @stacksjs/orm, storage/framework/orm/, and storage/framework/defaults/app/Models/.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Bun >= 1.3.0, TypeScript, SQLite >= 3.47.2
|
|
6
6
|
allowed-tools: Read Edit Write Bash Grep Glob
|
|
@@ -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/` (
|
|
14
|
+
- Model definitions: `storage/framework/defaults/app/Models/` (104 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`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stacks-security
|
|
3
|
-
description: Use when implementing security in Stacks - password hashing (bcrypt/argon2), app key generation, AES encryption/decryption, hash verification, rehashing detection,
|
|
3
|
+
description: Use when implementing security in Stacks - password hashing (bcrypt/argon2), app key generation, AES encryption/decryption, hash verification, rehashing detection, security configuration (firewall, rate limiting, IP allowlists), or GDPR data-subject requests (access export, erasure, retention, the processing register). Covers @stacksjs/security, config/security.ts and the GDPR layer in @stacksjs/orm.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Bun >= 1.3.0, TypeScript
|
|
6
6
|
allowed-tools: Read Edit Write Bash Grep Glob
|
|
@@ -116,6 +116,33 @@ interface HashMakeOptions {
|
|
|
116
116
|
}
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
## Personal data and GDPR
|
|
120
|
+
|
|
121
|
+
Data-subject requests are driven by model declarations, not by code you write per
|
|
122
|
+
request: `personal: true` on an attribute and `traits.gdpr` on the model (see
|
|
123
|
+
`stacks-models`, "Personal data (GDPR)", and `docs/guide/gdpr.md`).
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
buddy gdpr:export <id|email> --out file.json # access (Art. 15/20)
|
|
127
|
+
buddy gdpr:erase <id|email> --dry-run # then without --dry-run, or --yes
|
|
128
|
+
buddy gdpr:prune --dry-run # retention; PruneRetainedDataJob runs daily
|
|
129
|
+
buddy gdpr:register && buddy gdpr:register:check
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { eraseSubject, exportSubjectData, pruneRetainedData } from '@stacksjs/orm'
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- `GET /me/data-export` (auth bundle, `auth` middleware, 3/hour) is the
|
|
137
|
+
self-service export; the subject is always the caller.
|
|
138
|
+
- Erasure runs in one transaction with its `gdpr_requests` audit row, writes only
|
|
139
|
+
rows it matched to the subject, then revokes tokens and destroys sessions.
|
|
140
|
+
- Erasure of the `User` row anonymizes rather than deletes: orders, payments and
|
|
141
|
+
memberships still point at it. Consent and suppression records are kept by
|
|
142
|
+
declaration, since honouring an opt-out needs the address.
|
|
143
|
+
- The audit ledger stores counts, never values. Do not log export payloads with
|
|
144
|
+
`log`, which writes to disk in production.
|
|
145
|
+
|
|
119
146
|
## Gotchas
|
|
120
147
|
- Default hashing is bcrypt with 12 rounds — sufficient for most applications
|
|
121
148
|
- `needsRehash()` compares current hash options against provided options — useful after config changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { existsSync, unlinkSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /me/data-export, at the action seam, over the framework's own model
|
|
9
|
+
* declarations and a real SQLite database holding two users.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DB_PATH = join(tmpdir(), `stacks-data-export-${process.pid}.sqlite`)
|
|
13
|
+
process.env.DB_CONNECTION = 'sqlite'
|
|
14
|
+
process.env.DB_DATABASE_PATH = DB_PATH
|
|
15
|
+
process.env.APP_ENV = 'test'
|
|
16
|
+
|
|
17
|
+
const { acquireDbConfigLock, db, ensureDatabaseConfigLoaded, initializeDbConfig } = await import('@stacksjs/database')
|
|
18
|
+
const { default: DataExportAction } = await import('./DataExportAction')
|
|
19
|
+
|
|
20
|
+
async function forceConfig(): Promise<void> {
|
|
21
|
+
const release = await acquireDbConfigLock()
|
|
22
|
+
try {
|
|
23
|
+
await ensureDatabaseConfigLoaded()
|
|
24
|
+
initializeDbConfig({
|
|
25
|
+
app: { env: 'test' },
|
|
26
|
+
database: { default: 'sqlite', connections: { sqlite: { database: DB_PATH, prefix: '' } } },
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
release()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function requestAs(user: { id: number } | undefined): RequestInstance {
|
|
35
|
+
return { user: async () => user } as unknown as RequestInstance
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
beforeAll(async () => {
|
|
39
|
+
for (const suffix of ['', '-wal', '-shm']) {
|
|
40
|
+
if (existsSync(`${DB_PATH}${suffix}`))
|
|
41
|
+
unlinkSync(`${DB_PATH}${suffix}`)
|
|
42
|
+
}
|
|
43
|
+
await forceConfig()
|
|
44
|
+
await db.unsafe(`CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT, name TEXT, email TEXT, password TEXT, avatar TEXT, created_at TEXT, updated_at TEXT)`).execute()
|
|
45
|
+
await db.unsafe(`CREATE TABLE gdpr_requests (id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT, type TEXT NOT NULL, subject_id INTEGER, actor TEXT, status TEXT NOT NULL DEFAULT 'completed', summary TEXT, occurred_at TEXT NOT NULL, created_at TEXT, updated_at TEXT)`).execute()
|
|
46
|
+
await db.unsafe(`INSERT INTO users (name, email, password) VALUES ('Ada Lovelace', 'ada@example.com', 'hash-ada'), ('Bob Byron', 'bob@example.com', 'hash-bob')`).execute()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
afterAll(() => {
|
|
50
|
+
for (const suffix of ['', '-wal', '-shm']) {
|
|
51
|
+
try {
|
|
52
|
+
if (existsSync(`${DB_PATH}${suffix}`))
|
|
53
|
+
unlinkSync(`${DB_PATH}${suffix}`)
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// best effort
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('DataExportAction', () => {
|
|
62
|
+
test('downloads the caller\'s own data, without the password hash, and records the request', async () => {
|
|
63
|
+
await forceConfig()
|
|
64
|
+
const res = await DataExportAction.handle(requestAs({ id: 2 })) as Response
|
|
65
|
+
|
|
66
|
+
expect(res.status).toBe(200)
|
|
67
|
+
expect(res.headers.get('Content-Disposition')).toMatch(/^attachment; filename="personal-data-\d{4}-\d{2}-\d{2}\.json"$/)
|
|
68
|
+
expect(res.headers.get('Cache-Control')).toBe('private, no-store')
|
|
69
|
+
|
|
70
|
+
const body = await res.json() as { subject: { id: number }, data: Record<string, Array<Record<string, unknown>>> }
|
|
71
|
+
expect(body.subject).toEqual({ id: 2 })
|
|
72
|
+
expect(body.data.User).toEqual([expect.objectContaining({ id: 2, name: 'Bob Byron', email: 'bob@example.com' })])
|
|
73
|
+
expect(JSON.stringify(body)).not.toContain('hash-bob')
|
|
74
|
+
expect(JSON.stringify(body)).not.toContain('ada@example.com')
|
|
75
|
+
|
|
76
|
+
const audit = await (db as any).selectFrom('gdpr_requests').selectAll().execute()
|
|
77
|
+
expect(audit).toEqual([expect.objectContaining({ type: 'access', subject_id: 2, actor: 'api:user:2' })])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('refuses an unauthenticated caller', async () => {
|
|
81
|
+
const res = await DataExportAction.handle(requestAs(undefined)) as Response
|
|
82
|
+
expect(res.status).toBe(401)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Action } from '@stacksjs/actions'
|
|
2
|
+
import { log } from '@stacksjs/logging'
|
|
3
|
+
import { exportSubjectData } from '@stacksjs/orm'
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Self-service data export: the signed-in user downloads their own personal
|
|
8
|
+
* data (GDPR Articles 15 and 20, stacksjs/stacks#365).
|
|
9
|
+
*
|
|
10
|
+
* The subject is always the authenticated caller - there is no id parameter
|
|
11
|
+
* to tamper with. The export carries only what the models declare personal,
|
|
12
|
+
* plus ids, timestamps and the purpose/basis/retention each model states.
|
|
13
|
+
* Each one is recorded in `gdpr_requests` with the caller as actor, and the
|
|
14
|
+
* route is rate-limited because an export reads every table the user touches.
|
|
15
|
+
*/
|
|
16
|
+
export default new Action({
|
|
17
|
+
name: 'DataExportAction',
|
|
18
|
+
description: 'Download the authenticated user\'s personal data',
|
|
19
|
+
method: 'GET',
|
|
20
|
+
async handle(request: RequestInstance) {
|
|
21
|
+
const user = await request.user()
|
|
22
|
+
|
|
23
|
+
if (!user?.id)
|
|
24
|
+
return response.unauthorized('Authentication required')
|
|
25
|
+
|
|
26
|
+
const result = await exportSubjectData(user.id, { actor: `api:user:${String(user.id)}` })
|
|
27
|
+
log.info(`[gdpr] access export served to user ${String(user.id)}`)
|
|
28
|
+
|
|
29
|
+
const day = result.generatedAt.slice(0, 10)
|
|
30
|
+
return new Response(`${JSON.stringify(result, null, 2)}\n`, {
|
|
31
|
+
status: 200,
|
|
32
|
+
headers: {
|
|
33
|
+
'Cache-Control': 'private, no-store',
|
|
34
|
+
'Content-Disposition': `attachment; filename="personal-data-${day}.json"`,
|
|
35
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
36
|
+
'X-Content-Type-Options': 'nosniff',
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
})
|
|
@@ -5,7 +5,7 @@ import { DashboardFileError, reprocessDashboardFile } from './file-manager'
|
|
|
5
5
|
|
|
6
6
|
export default new Action({
|
|
7
7
|
name: 'FileReprocessAction',
|
|
8
|
-
description: 'Re-runs the background processing for a file: image variants, video renditions, AI tags.',
|
|
8
|
+
description: 'Re-runs the background processing for a file: image variants, video renditions, AI tags, previews.',
|
|
9
9
|
method: 'POST',
|
|
10
10
|
async handle(request: RequestInstance) {
|
|
11
11
|
try {
|