From 21d0f291e3b041daa936247c97f1d1770d588e24 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 13:17:45 +0000 Subject: [PATCH 1/3] feat: update to @haverstack/core 0.7.0, simplify attachment routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump @haverstack/core dependency to 0.7.0 and simplify the GET and DELETE attachment routes to delegate to the new Stack/ScopedStack methods instead of reimplementing the access control and cleanup logic inline. - GET /attachments/:fileId now uses ScopedStack.getAttachment(), removing the standalone isAttachmentAccessible helper (~30 lines) - DELETE /attachments/:fileId now uses Stack.deleteAttachment(), collapsing ~20 lines of query/check/loop logic into a single call - Add StackConflictError → 409 to the error middleware Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- pnpm-lock.yaml | 9 +++-- src/middleware/errors.ts | 3 +- src/routes/attachments.ts | 74 +++------------------------------------ 4 files changed, 15 insertions(+), 73 deletions(-) diff --git a/package.json b/package.json index 33b90b8..4569008 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "@haverstack/adapter-local": "^0.6.0", - "@haverstack/core": "^0.6.0", + "@haverstack/core": "^0.7.0", "@haverstack/wire-types": "^0.6.0", "@hono/node-server": "^1.13.7", "hono": "^4.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2e4844..b852057 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^0.6.0 version: 0.6.0 '@haverstack/core': - specifier: ^0.6.0 - version: 0.6.0 + specifier: ^0.7.0 + version: 0.7.0 '@haverstack/wire-types': specifier: ^0.6.0 version: 0.6.0 @@ -410,6 +410,9 @@ packages: '@haverstack/core@0.6.0': resolution: {integrity: sha512-ruv1LrOlCaCgQTfUKbQEi3hYeIxJ7fGHU2ynWPY3LjB5Cia+bbMkAnPtznEparunPNhOfdI/nf51MjMwyhFhkg==} + '@haverstack/core@0.7.0': + resolution: {integrity: sha512-qIvLQ3NulWR6JPSbE40aXFiNbfqiksIo8TxuDS2hwVPB4Jl2z5WdCQefk38m0DCybOrCVjREVYP9rUQ+y8ZHbg==} + '@haverstack/record-adapter-sqljs@0.6.0': resolution: {integrity: sha512-+BHfNqW+dKg0Yy+io6cvqXjrOWugBXbPtQJWKTI9VNA6zfHngoWPpYIJ9n01dQ8LpsorXaRAG0ROO3Fev9mI6Q==} @@ -1515,6 +1518,8 @@ snapshots: '@haverstack/core@0.6.0': {} + '@haverstack/core@0.7.0': {} + '@haverstack/record-adapter-sqljs@0.6.0': dependencies: '@haverstack/core': 0.6.0 diff --git a/src/middleware/errors.ts b/src/middleware/errors.ts index 2cd45bc..9d43407 100644 --- a/src/middleware/errors.ts +++ b/src/middleware/errors.ts @@ -1,12 +1,13 @@ import type { ErrorHandler } from 'hono'; import type { Logger } from 'pino'; import type { AppEnv } from '../types.js'; -import { StackPermissionError, StackValidationError, StackNotFoundError } from '@haverstack/core'; +import { StackPermissionError, StackValidationError, StackNotFoundError, StackConflictError } from '@haverstack/core'; export function errorMiddleware(logger: Logger): ErrorHandler { return (err, c) => { if (err instanceof StackNotFoundError) return c.json({ error: 'Not found' }, 404); if (err instanceof StackPermissionError) return c.json({ error: 'Forbidden' }, 403); + if (err instanceof StackConflictError) return c.json({ error: err.message }, 409); if (err instanceof StackValidationError) return c.json({ error: err.message, details: err.errors }, 422); logger.error({ err, requestId: c.get('requestId') }, 'Unhandled request error'); diff --git a/src/routes/attachments.ts b/src/routes/attachments.ts index 160272d..d27684a 100644 --- a/src/routes/attachments.ts +++ b/src/routes/attachments.ts @@ -1,5 +1,5 @@ import { Hono } from 'hono'; -import { SYSTEM_TYPES } from '@haverstack/core'; +import { StackPermissionError } from '@haverstack/core'; import type { AppEnv } from '../types.js'; import type { StackContext } from '../stack.js'; import { requireAuth, requireOwner } from '../middleware/auth.js'; @@ -33,13 +33,11 @@ export function attachmentRoutes(ctx: StackContext, maxAttachmentBytes: number): const fileId = c.req.param('fileId'); const auth = c.get('auth'); - const accessible = await isAttachmentAccessible(fileId, auth?.entityId ?? null, ctx); - if (!accessible) return c.json({ error: 'Unauthorized' }, 401); - let data: Uint8Array; try { - data = await adapter.getAttachment(fileId); - } catch { + data = await stack.asEntity(auth?.entityId ?? null).getAttachment(fileId); + } catch (e) { + if (e instanceof StackPermissionError) return c.json({ error: 'Unauthorized' }, 401); return c.json({ error: 'Attachment not found' }, 404); } @@ -63,32 +61,7 @@ export function attachmentRoutes(ctx: StackContext, maxAttachmentBytes: number): // DELETE /attachments/:fileId app.delete('/:fileId', requireOwner(ownerEntityId), async (c) => { const fileId = c.req.param('fileId'); - - // Find any _attachment@1 metadata record(s) for this file - const metaResult = await stack.query({ - filter: { typeId: `${SYSTEM_TYPES.ATTACHMENT}@1`, content: { fileId } }, - }); - - // If no metadata record, verify the bytes actually exist before proceeding - if (!metaResult.records.length) { - try { - await adapter.getAttachment(fileId); - } catch { - return c.json({ error: 'Attachment not found' }, 404); - } - } - - // Refuse if any record in the stack still references this file - const refResult = await stack.query({ filter: { attachmentFileId: fileId }, limit: 1 }); - if (refResult.records.length > 0) { - return c.json({ error: 'Attachment is still referenced by one or more records' }, 409); - } - - for (const record of metaResult.records) { - await stack.delete(record.id, { hard: true }); - } - - await adapter.deleteAttachment(fileId); + await stack.deleteAttachment(fileId); return c.body(null, 204); }); @@ -143,40 +116,3 @@ function resolveMimeType(declared: string, filename: string | undefined): string const ext = filename.split('.').pop()?.toLowerCase(); return (ext && EXTENSION_MIME[ext]) || declared; } - -/** - * An attachment is accessible if: - * 1. The requester is the stack owner, OR - * 2. The requester can read at least one Record that references the file, OR - * 3. The requester uploaded the file (owns its _attachment@1 record) and it - * hasn't been associated with any record yet. - */ -async function isAttachmentAccessible( - fileId: string, - requesterEntityId: string | null, - ctx: StackContext, -): Promise { - const { stack } = ctx; - if (requesterEntityId && requesterEntityId === stack.ownerEntityId) return true; - - const result = await stack.asEntity(requesterEntityId).query({ - filter: { attachmentFileId: fileId }, - limit: 1, - }); - if (result.records.length > 0) return true; - - // Unassociated file: check if the requester uploaded it - if (requesterEntityId) { - const uploadResult = await stack.query({ - filter: { - typeId: `${SYSTEM_TYPES.ATTACHMENT}@1`, - entityId: requesterEntityId, - content: { fileId }, - }, - limit: 1, - }); - if (uploadResult.records.length > 0) return true; - } - - return false; -} From 5a9c2b0e7f158289001428eddb739fbfb173e9c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 13:49:58 +0000 Subject: [PATCH 2/3] chore: bump @haverstack/core to 0.8.0 Picks up the StackConflictError public export fix. Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 4569008..7cb300f 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "@haverstack/adapter-local": "^0.6.0", - "@haverstack/core": "^0.7.0", + "@haverstack/core": "^0.8.0", "@haverstack/wire-types": "^0.6.0", "@hono/node-server": "^1.13.7", "hono": "^4.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b852057..f4c0b0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^0.6.0 version: 0.6.0 '@haverstack/core': - specifier: ^0.7.0 - version: 0.7.0 + specifier: ^0.8.0 + version: 0.8.0 '@haverstack/wire-types': specifier: ^0.6.0 version: 0.6.0 @@ -410,8 +410,8 @@ packages: '@haverstack/core@0.6.0': resolution: {integrity: sha512-ruv1LrOlCaCgQTfUKbQEi3hYeIxJ7fGHU2ynWPY3LjB5Cia+bbMkAnPtznEparunPNhOfdI/nf51MjMwyhFhkg==} - '@haverstack/core@0.7.0': - resolution: {integrity: sha512-qIvLQ3NulWR6JPSbE40aXFiNbfqiksIo8TxuDS2hwVPB4Jl2z5WdCQefk38m0DCybOrCVjREVYP9rUQ+y8ZHbg==} + '@haverstack/core@0.8.0': + resolution: {integrity: sha512-QT4YvvpZRwHILJCUAsIA28Ed043/nTP97+njEFAr3fOb3B2kCGT14WCdobCjCy4y2uX6ziwch7/lzGlmfirI8Q==} '@haverstack/record-adapter-sqljs@0.6.0': resolution: {integrity: sha512-+BHfNqW+dKg0Yy+io6cvqXjrOWugBXbPtQJWKTI9VNA6zfHngoWPpYIJ9n01dQ8LpsorXaRAG0ROO3Fev9mI6Q==} @@ -1518,7 +1518,7 @@ snapshots: '@haverstack/core@0.6.0': {} - '@haverstack/core@0.7.0': {} + '@haverstack/core@0.8.0': {} '@haverstack/record-adapter-sqljs@0.6.0': dependencies: From 4a380de67f8336cdaab64cfbc73ea3310fbb058c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 13:55:22 +0000 Subject: [PATCH 3/3] style: format errors.ts import Co-Authored-By: Claude Sonnet 4.6 --- src/middleware/errors.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/middleware/errors.ts b/src/middleware/errors.ts index 9d43407..6460b61 100644 --- a/src/middleware/errors.ts +++ b/src/middleware/errors.ts @@ -1,7 +1,12 @@ import type { ErrorHandler } from 'hono'; import type { Logger } from 'pino'; import type { AppEnv } from '../types.js'; -import { StackPermissionError, StackValidationError, StackNotFoundError, StackConflictError } from '@haverstack/core'; +import { + StackPermissionError, + StackValidationError, + StackNotFoundError, + StackConflictError, +} from '@haverstack/core'; export function errorMiddleware(logger: Logger): ErrorHandler { return (err, c) => {