Nuvaka › Developer docs › Core contributions

Core contributions

Your extension can add options to Nuvaka's own screens: an action like "Edit" in a file's menu in My Files, or a row in the "Open with" submenu. When the user picks it, your extension's page opens with a time-limited handle that can access only that one file.

Contribution points

PointWhereDefault mode
storage.fileActionMy Files › file menu (e.g. "Edit")readwrite
storage.openWithMy Files › file menu › Open withread
"entry": { "ui": "index.html" },
"pages": [{ "id": "main", "title": { "tr": "Düzenleyici", "en": "Editor" } }],
"contributes": {
  "storage.fileAction": [
    { "id": "edit", "title": { "tr": "Düzenle", "en": "Edit" }, "icon": "pencil", "extensions": ["txt", "md"], "page": "main" }
  ],
  "storage.openWith": [
    { "id": "view", "title": { "tr": "Görüntüle", "en": "View" }, "mimeTypes": ["text/*"], "page": "main", "mode": "read" }
  ]
}

Field rules: Manifest › contributes. An item appears when the file's extension (extensions) or MIME type (mimeTypes, wildcards like image/* included) matches.

Nuvaka draws the item

  • The menu row is drawn by Nuvaka's UI: the title from the manifest, the icon and your extension's name. An extension cannot put HTML, styles or code on a Nuvaka screen; it sees nothing until the user picks the item.
  • Users can turn contributions off and on per extension (in the Apps screen, "Additions to Nuvaka screens"). A disabled contribution does not appear in the menu.
  • If several extensions offer "Open with" for the same file type, the user can make one the default for that type.
  • The Store card and detail page show that the extension adds to Nuvaka screens; it is also listed as a separate row at the top of the permission list.
  • Every version with contributes goes through Nuvaka review. An update that changes or removes contributions is not installed automatically; the user is asked.

Single-file handle

When the user picks a contribution, the app obtains a handle for that file with the user's session and opens your extension's page. The handle:

  • is valid only for this user and this extension; no other extension can use it;
  • accesses only that one file; no nuvaka.cloud permission is needed;
  • has a mode: read or readwrite. The upper bound is the contribution's mode (default: storage.fileAction → readwrite, storage.openWith → read); a wider request is refused with read_only_contribution;
  • has a sliding lifetime: 15 minutes at open; every read, write or meta() extends it by 15 minutes; at most 8 hours from opening. An editor that stays open for a long time can keep the handle alive by calling meta() now and then. The current expiry is meta().expiresAt;
  • is never written to the address bar or history; every use is recorded in the extension's activity log.

SDK: nuvaka.files.handle

If the page was opened through a contribution, the context carries handle and fileName. If the user picks the contribution on another file while the page is open, the file.open event fires and the context is updated.

const ctx = await nuvaka.ready
if (ctx.handle) await openFile(ctx.handle)
nuvaka.on('file.open', ({ handle }) => openFile(handle))

async function openFile(h) {
  const file = nuvaka.files.handle(h)
  const meta = await file.meta()        // { fileId, name, size, contentType, version, updatedAt, mode, contribution, expiresAt }
  editor.value = await file.readText()  // or read() → Uint8Array
  saveButton.disabled = meta.mode !== 'readwrite'
  saveButton.onclick = async () => {
    try {
      const r = await file.write(editor.value, { version: meta.version, contentType: 'text/plain' })
      meta.version = r.file.version     // for the next save
      showInline(`Saved. Previous content: ${r.previous.name}`)
    } catch (e) {
      if (e.code === 'version_conflict') showInline('The file changed elsewhere; reload it.')   // e.data.current = current Meta
      else throw e
    }
  }
}
CallReturns
nuvaka.files.handle(h)handle object (synchronous; no network call)
.meta(){ fileId, name, size, contentType, version, updatedAt, mode, contribution, expiresAt }
.read()Uint8Array
.readText()text (UTF-8)
.write(data, { version, contentType? }){ previous: { fileId, name }, file: Meta }; readwrite only; at most 100 MB

Writing: MVCC and the previous version

  • write needs the version you read (meta().version). If the file changed after you read it, the call fails with version_conflict and the error carries the file's current state (e.data.current; VersionConflictError.file in the SDK). Without a version: if_version_required.
  • The old content is not deleted: it is kept in the same folder as "<name> (önceki sürüm yyyy-MM-dd HH.mm.ss).<ext>" ("previous version"). The new content becomes a new file under the original name and the handle moves to it (fileId changes). Share links stay on the old file.

Errors

CodeMeaning
bad_handlethe handle is invalid, expired or belongs to another extension
read_onlya write was attempted with a read handle
version_conflictthe file changed after you read it
if_version_requiredwrite was called without a version

Cases that can happen while opening the handle (app side): the contribution does not match this file type (no_contribution), the user disabled it (contribution_disabled), a mode wider than the contribution's (read_only_contribution), the extension is stopped or blocked. Nuvaka shows these to the user.

Testing

The SDK's mock nuvaka simulates handles: sliding lifetime, MVCC, the previous-version name, read_only.

import { installMockNuvaka } from '@nuvaka/extension-sdk/testing'

const m = installMockNuvaka({ file: { name: 'note.txt', content: 'hello', contentType: 'text/plain', contribution: 'edit' } })
const h = nuvaka.files.handle(nuvaka.context().handle)
m.remote.editFile(nuvaka.context().handle, 'changed by someone else')   // next write conflicts
await m.openFile({ name: 'b.md', content: '# b' })                       // file.open event
m.log.fileWrites                                                          // write records

Nuvaka Apps API v1 · last updated 2026-09-27