Skip to content

fix(ntask-core): correct the attachment insert, and add the presign actions - #11

Merged
acamarata merged 2 commits into
mainfrom
fix/attachment-bucket-not-insertable
Aug 22, 2026
Merged

fix(ntask-core): correct the attachment insert, and add the presign actions#11
acamarata merged 2 commits into
mainfrom
fix/attachment-bucket-not-insertable

Conversation

@acamarata

@acamarata acamarata commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Two changes to @nself/ntask-core's attachment layer, both driven by making attachments actually work end to end.

1. bucket must not be sent on insert

CREATE_ATTACHMENT declared $bucket: String! and passed it in the insert object. bucket is not client-insertable.

getDownloadUrl honours attachment.bucket and then signs the storage key with the storage root credentials, so a caller-chosen bucket allowed cross-bucket traversal: register a row pointing at another user's object, receive a working presigned URL for it. The Hasura insert permission drops the column and the database default (ntask) applies, so the mutation as written now fails with:

field 'bucket' not found in type: 'np_attachments_insert_input'

Nothing consumed this operation yet — the web app carried its own hand-written copy — which is why it was not already failing. It would have broken the mobile attachment work about to use it.

bucket stays in the response selection so callers can see what the server chose. uploader_id is documented as absent-by-design (Hasura presets it to the caller).

2. The presign actions were missing entirely

GET_UPLOAD_URL / GET_DOWNLOAD_URL existed only inside the web app, so any other surface had to reinvent them — which is precisely how the field-name drift happened in the first place. Signatures taken from backend/hasura/metadata/actions.graphql.

Note the Action arguments are String!, not uuid!: they are Action arguments rather than columns, so the uuid scalar does not apply.

The three-step upload contract is documented on the export, because skipping step 3 leaves an orphaned object no query will ever return, and adding an Authorization header to the PUT invalidates the signature.

Regression tests

Assert field names against the real schema: never filename/size_bytes/user_id, never bucket or uploader_id on insert, and the action argument types.

I verified the guard actually fails by reintroducing bucket and watching the suite go red, then restoring. That check matters here — the original web bug survived because its fixtures repeated the same wrong column names the client used, so the test and the client agreed with each other while both disagreed with the server. A guard that cannot fail is worse than none.

63 tests pass; tsc build clean; lefthook typecheck + license gates green.

CREATE_ATTACHMENT declared `$bucket: String!` and passed it in the insert
object. `bucket` is not client-insertable: getDownloadUrl honours
attachment.bucket and then signs the storage key with the storage root
credentials, so a caller-chosen bucket allowed cross-bucket traversal —
a user could register a row pointing at another user's object and receive
a presigned URL for it.

The Hasura permission now drops the column, so the mutation as written
fails with:

  field 'bucket' not found in type: 'np_attachments_insert_input'

Nothing consumes it yet (the web app carried its own copy), which is why
this was not already breaking. It would have broken the mobile attachment
work that is about to use it.

`bucket` stays in the response selection so callers can see what the
server chose, and CreateAttachmentInput drops it to match.

Adds a regression test asserting the field names against the real schema:
never filename/size_bytes/user_id, never bucket or uploader_id on insert.
Verified the guard actually fires by reintroducing bucket and watching it
fail. That check matters — the original web bug survived because its
fixtures repeated the same wrong column names the client used, so the test
and the client agreed while both disagreed with the server.
GET_UPLOAD_URL and GET_DOWNLOAD_URL existed only as a private copy inside
the web app, so any other surface wiring attachments had to reinvent them.
That private copy is exactly how the field-name drift happened.

Signatures taken from backend/hasura/metadata/actions.graphql. Note the
Action arguments are String!, not uuid! — they are Action arguments rather
than columns, so the uuid scalar does not apply.

The three-step upload contract is documented on the export, because
skipping step 3 leaves an orphaned object no query will ever return, and
adding an Authorization header to the PUT invalidates the signature.
@acamarata acamarata changed the title fix(ntask-core): stop sending bucket on attachment insert fix(ntask-core): correct the attachment insert, and add the presign actions Aug 22, 2026
@acamarata

Copy link
Copy Markdown
Contributor Author

Merging on local verification rather than waiting on the hosted run.

Why: the org has exactly one self-hosted runner (nself-staging-runner) serving every repo, and staging is CPU-saturated (load 4.4 on 4 cores) with 10 jobs queued across packages and web. This PR's run sat queued for 6+ minutes with no runner available. Adding runners to that box would make the saturation worse, not better.

main here has no branch protection, so nothing is being bypassed. I ran every step of P3 Workspace CI locally instead:

Step Result
pnpm install --frozen-lockfile pass
pnpm audit --audit-level=high 2 high, both already ignored — same as CI sees
pnpm typecheck pass
pnpm test --filter "@nself/*" 29/29 tasks
pnpm check:licenses pass
pnpm codegen:check pass, working tree clean afterwards (no drift)
schema:check pass

Plus the lefthook pre-commit gates (license-check, typecheck, test) on each commit.

The runner shortage is a separate problem worth fixing on its own terms — it is currently the throughput ceiling for every repo in the org.

@acamarata
acamarata merged commit 2d5f1dd into main Aug 22, 2026
3 checks passed
@acamarata
acamarata deleted the fix/attachment-bucket-not-insertable branch August 22, 2026 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant