Compare commits

...

8 Commits

Author SHA1 Message Date
Corentin Thomasset
0616635cd6 chore(release): update versions (#509)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-24 17:00:01 +02:00
Corentin Thomasset
9e7a3ba70b chore(version): update version bump for api keys permissions changes (#516) 2025-09-24 16:49:11 +02:00
Corentin Thomasset
04990b986e docs(api-endpoints): added explications on how to use api keys (#515) 2025-09-24 14:41:14 +00:00
Corentin Thomasset
097b6bf2b7 feat(api-keys): added format check for api tokens to avoid unnecessary db call (#514) 2025-09-24 14:32:34 +00:00
Corentin Thomasset
cb3ce6b1d8 feat(api-keys): add organization permissions for api keys (#512) 2025-09-24 15:25:48 +02:00
Corentin Thomasset
405ba645f6 feat(docker): disable Better Auth telemetry in Dockerfiles (#511) 2025-09-21 20:56:43 +00:00
Corentin Thomasset
ab6fd6ad10 feat(tasks): update figue to allow for fallback task worker ids env variables (#510) 2025-09-21 22:53:04 +02:00
Corentin Thomasset
782f70ff66 feat(tasks): add option to disable PRAGMA statements in migrations (#508) 2025-09-20 22:07:34 +00:00
30 changed files with 392 additions and 74 deletions

View File

@@ -1,5 +1,11 @@
# @papra/docs
## 0.6.1
### Patch Changes
- [#512](https://github.com/papra-hq/papra/pull/512) [`cb3ce6b`](https://github.com/papra-hq/papra/commit/cb3ce6b1d8d5dba09cbf0d2964f14b1c93220571) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added organizations permissions for api keys
## 0.6.0
### Minor Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@papra/docs",
"type": "module",
"version": "0.6.0",
"version": "0.6.1",
"private": true,
"packageManager": "pnpm@10.12.3",
"description": "Papra documentation website",
@@ -37,7 +37,7 @@
"@unocss/reset": "^0.64.0",
"eslint": "^9.17.0",
"eslint-plugin-astro": "^1.3.1",
"figue": "^2.2.2",
"figue": "^3.1.1",
"lodash-es": "^4.17.21",
"marked": "^15.0.6",
"typescript": "^5.7.3",

View File

@@ -1,5 +1,5 @@
import type { ConfigDefinition, ConfigDefinitionElement } from 'figue';
import { isArray, isEmpty, isNil } from 'lodash-es';
import { castArray, isArray, isEmpty, isNil } from 'lodash-es';
import { marked } from 'marked';
import { configDefinition } from '../../papra-server/src/modules/config/config';
@@ -46,16 +46,21 @@ const rows = configDetails
};
});
const mdSections = rows.map(({ documentation, env, path, defaultValue }) => `
### ${env}
const mdSections = rows.map(({ documentation, env, path, defaultValue }) => {
const envs = castArray(env);
const [firstEnv, ...restEnvs] = envs;
return `
### ${firstEnv}
${documentation}
- Path: \`${path.join('.')}\`
- Environment variable: \`${env}\`
- Environment variable: \`${firstEnv}\` ${restEnvs.length ? `, with fallback to: ${restEnvs.map(e => `\`${e}\``).join(', ')}` : ''}
- Default value: \`${defaultValue}\`
`.trim()).join('\n\n---\n\n');
`.trim();
}).join('\n\n---\n\n');
function wrapText(text: string, maxLength = 75) {
const words = text.split(' ');
@@ -80,10 +85,12 @@ function wrapText(text: string, maxLength = 75) {
const fullDotEnv = rows.map(({ env, defaultValue, documentation }) => {
const isEmptyDefaultValue = isNil(defaultValue) || (isArray(defaultValue) && isEmpty(defaultValue)) || defaultValue === '';
const envs = castArray(env);
const [firstEnv] = envs;
return [
...wrapText(documentation),
`# ${env}=${isEmptyDefaultValue ? '' : defaultValue}`,
`# ${firstEnv}=${isEmptyDefaultValue ? '' : defaultValue}`,
].join('\n');
}).join('\n\n');

View File

@@ -18,8 +18,107 @@ The public API uses a bearer token for authentication. You can get a token by lo
</details>
To authenticate your requests, include the token in the `Authorization` header with the `Bearer` prefix:
```
Authorization: Bearer YOUR_API_TOKEN
```
### Examples
**Using cURL:**
```bash
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://api.papra.app/api/organizations
```
**Using JavaScript (fetch):**
```javascript
const response = await fetch('https://api.papra.app/api/organizations', {
headers: {
'Authorization': 'Bearer YOUR_API_TOKEN',
'Content-Type': 'application/json'
}
})
```
### API Key Permissions
When creating an API key, you can select from the following permissions:
**Organizations:**
- `organizations:create` - Create new organizations
- `organizations:read` - Read organization information and list organizations of the user
- `organizations:update` - Update organization details
- `organizations:delete` - Delete organizations
**Documents:**
- `documents:create` - Upload and create new documents
- `documents:read` - Read and download documents
- `documents:update` - Update document metadata and content
- `documents:delete` - Delete documents
**Tags:**
- `tags:create` - Create new tags
- `tags:read` - Read tag information and list tags
- `tags:update` - Update tag details
- `tags:delete` - Delete tags
## Endpoints
### List organizations
**GET** `/api/organizations`
List all organizations accessible to the authenticated user.
- Required API key permissions: `organizations:read`
- Response (JSON)
- `organizations`: The list of organizations.
### Create an organization
**POST** `/api/organizations`
Create a new organization.
- Required API key permissions: `organizations:create`
- Body (JSON)
- `name`: The organization name (3-50 characters).
- Response (JSON)
- `organization`: The created organization.
### Get an organization
**GET** `/api/organizations/:organizationId`
Get an organization by its ID.
- Required API key permissions: `organizations:read`
- Response (JSON)
- `organization`: The organization.
### Update an organization
**PUT** `/api/organizations/:organizationId`
Update an organization's name.
- Required API key permissions: `organizations:update`
- Body (JSON)
- `name`: The new organization name (3-50 characters).
- Response (JSON)
- `organization`: The updated organization.
### Delete an organization
**DELETE** `/api/organizations/:organizationId`
Delete an organization by its ID.
- Required API key permissions: `organizations:delete`
- Response: empty (204 status code)
### Create a document
**POST** `/api/organizations/:organizationId/documents`

View File

@@ -1,5 +1,11 @@
# @papra/app-client
## 0.9.4
### Patch Changes
- [#512](https://github.com/papra-hq/papra/pull/512) [`cb3ce6b`](https://github.com/papra-hq/papra/commit/cb3ce6b1d8d5dba09cbf0d2964f14b1c93220571) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added organizations permissions for api keys
## 0.9.3
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@papra/app-client",
"type": "module",
"version": "0.9.3",
"version": "0.9.4",
"private": true,
"packageManager": "pnpm@10.12.3",
"description": "Papra frontend client",

View File

@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
// API keys
'api-keys.permissions.select-all': 'Alle auswählen',
'api-keys.permissions.deselect-all': 'Alle abwählen',
'api-keys.permissions.organizations.title': 'Organisationen',
'api-keys.permissions.organizations.organizations:create': 'Organisationen erstellen',
'api-keys.permissions.organizations.organizations:read': 'Organisationen lesen',
'api-keys.permissions.organizations.organizations:update': 'Organisationen aktualisieren',
'api-keys.permissions.organizations.organizations:delete': 'Organisationen löschen',
'api-keys.permissions.documents.title': 'Dokumente',
'api-keys.permissions.documents.documents:create': 'Dokumente erstellen',
'api-keys.permissions.documents.documents:read': 'Dokumente lesen',

View File

@@ -415,6 +415,13 @@ export const translations = {
// API keys
'api-keys.permissions.select-all': 'Select all',
'api-keys.permissions.deselect-all': 'Deselect all',
'api-keys.permissions.organizations.title': 'Organizations',
'api-keys.permissions.organizations.organizations:create': 'Create organizations',
'api-keys.permissions.organizations.organizations:read': 'Read organizations',
'api-keys.permissions.organizations.organizations:update': 'Update organizations',
'api-keys.permissions.organizations.organizations:delete': 'Delete organizations',
'api-keys.permissions.documents.title': 'Documents',
'api-keys.permissions.documents.documents:create': 'Create documents',
'api-keys.permissions.documents.documents:read': 'Read documents',

View File

@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
// API keys
'api-keys.permissions.select-all': 'Seleccionar todo',
'api-keys.permissions.deselect-all': 'Deseleccionar todo',
'api-keys.permissions.organizations.title': 'Organizaciones',
'api-keys.permissions.organizations.organizations:create': 'Crear organizaciones',
'api-keys.permissions.organizations.organizations:read': 'Leer organizaciones',
'api-keys.permissions.organizations.organizations:update': 'Actualizar organizaciones',
'api-keys.permissions.organizations.organizations:delete': 'Eliminar organizaciones',
'api-keys.permissions.documents.title': 'Documentos',
'api-keys.permissions.documents.documents:create': 'Crear documentos',
'api-keys.permissions.documents.documents:read': 'Leer documentos',

View File

@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
// API keys
'api-keys.permissions.select-all': 'Tout sélectionner',
'api-keys.permissions.deselect-all': 'Tout désélectionner',
'api-keys.permissions.organizations.title': 'Organisations',
'api-keys.permissions.organizations.organizations:create': 'Créer des organisations',
'api-keys.permissions.organizations.organizations:read': 'Lire des organisations',
'api-keys.permissions.organizations.organizations:update': 'Mettre à jour des organisations',
'api-keys.permissions.organizations.organizations:delete': 'Supprimer des organisations',
'api-keys.permissions.documents.title': 'Documents',
'api-keys.permissions.documents.documents:create': 'Créer des documents',
'api-keys.permissions.documents.documents:read': 'Lire des documents',

View File

@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
// API keys
'api-keys.permissions.select-all': 'Seleziona tutto',
'api-keys.permissions.deselect-all': 'Deseleziona tutto',
'api-keys.permissions.organizations.title': 'Organizzazioni',
'api-keys.permissions.organizations.organizations:create': 'Crea organizzazioni',
'api-keys.permissions.organizations.organizations:read': 'Leggi organizzazioni',
'api-keys.permissions.organizations.organizations:update': 'Aggiorna organizzazioni',
'api-keys.permissions.organizations.organizations:delete': 'Elimina organizzazioni',
'api-keys.permissions.documents.title': 'Documenti',
'api-keys.permissions.documents.documents:create': 'Crea documenti',
'api-keys.permissions.documents.documents:read': 'Leggi documenti',

View File

@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
// API keys
'api-keys.permissions.select-all': 'Zaznacz wszystko',
'api-keys.permissions.deselect-all': 'Odznacz wszystko',
'api-keys.permissions.organizations.title': 'Organizacje',
'api-keys.permissions.organizations.organizations:create': 'Tworzenie organizacji',
'api-keys.permissions.organizations.organizations:read': 'Odczyt organizacji',
'api-keys.permissions.organizations.organizations:update': 'Aktualizacja organizacji',
'api-keys.permissions.organizations.organizations:delete': 'Usuwanie organizacji',
'api-keys.permissions.documents.title': 'Dokumenty',
'api-keys.permissions.documents.documents:create': 'Tworzenie dokumentów',
'api-keys.permissions.documents.documents:read': 'Odczyt dokumentów',

View File

@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
// API keys
'api-keys.permissions.select-all': 'Selecionar tudo',
'api-keys.permissions.deselect-all': 'Desmarcar tudo',
'api-keys.permissions.organizations.title': 'Organizações',
'api-keys.permissions.organizations.organizations:create': 'Criar organizações',
'api-keys.permissions.organizations.organizations:read': 'Ler organizações',
'api-keys.permissions.organizations.organizations:update': 'Atualizar organizações',
'api-keys.permissions.organizations.organizations:delete': 'Excluir organizações',
'api-keys.permissions.documents.title': 'Documentos',
'api-keys.permissions.documents.documents:create': 'Criar documentos',
'api-keys.permissions.documents.documents:read': 'Ler documentos',

View File

@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
// API keys
'api-keys.permissions.select-all': 'Selecionar tudo',
'api-keys.permissions.deselect-all': 'Desselecionar tudo',
'api-keys.permissions.organizations.title': 'Organizações',
'api-keys.permissions.organizations.organizations:create': 'Criar organizações',
'api-keys.permissions.organizations.organizations:read': 'Ler organizações',
'api-keys.permissions.organizations.organizations:update': 'Atualizar organizações',
'api-keys.permissions.organizations.organizations:delete': 'Eliminar organizações',
'api-keys.permissions.documents.title': 'Documentos',
'api-keys.permissions.documents.documents:create': 'Criar documentos',
'api-keys.permissions.documents.documents:read': 'Ler documentos',

View File

@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
// API keys
'api-keys.permissions.select-all': 'Selectează tot',
'api-keys.permissions.deselect-all': 'Deselectează tot',
'api-keys.permissions.organizations.title': 'Organizații',
'api-keys.permissions.organizations.organizations:create': 'Creează organizații',
'api-keys.permissions.organizations.organizations:read': 'Citește organizații',
'api-keys.permissions.organizations.organizations:update': 'Actualizează organizații',
'api-keys.permissions.organizations.organizations:delete': 'Șterge organizații',
'api-keys.permissions.documents.title': 'Documente',
'api-keys.permissions.documents.documents:create': 'Creează documente',
'api-keys.permissions.documents.documents:read': 'Citește documente',

View File

@@ -5,6 +5,15 @@
// } as const;
export const API_KEY_PERMISSIONS = [
{
section: 'organizations',
permissions: [
'organizations:create',
'organizations:read',
'organizations:update',
'organizations:delete',
],
},
{
section: 'documents',
permissions: [

View File

@@ -2,6 +2,7 @@ import type { Component } from 'solid-js';
import type { TranslationKeys } from '@/modules/i18n/locales.types';
import { createSignal, For } from 'solid-js';
import { useI18n } from '@/modules/i18n/i18n.provider';
import { Button } from '@/modules/ui/components/button';
import { Checkbox, CheckboxControl, CheckboxLabel } from '@/modules/ui/components/checkbox';
import { API_KEY_PERMISSIONS } from '../api-keys.constants';
@@ -42,34 +43,78 @@ export const ApiKeyPermissionsPicker: Component<{ permissions: string[]; onChang
props.onChange(permissions());
};
return (
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<For each={getPermissionsSections()}>
{section => (
<div>
<p class="text-muted-foreground text-xs">{section.title}</p>
const toggleSection = (sectionName: typeof API_KEY_PERMISSIONS[number]['section']) => {
const section = API_KEY_PERMISSIONS.find(s => s.section === sectionName);
if (!section) {
return;
}
<div class="pl-4 flex flex-col gap-4 mt-4">
<For each={section.permissions}>
{permission => (
<Checkbox
class="flex items-center gap-2"
checked={isPermissionSelected(permission.name)}
onChange={() => togglePermission(permission.name)}
>
<CheckboxControl />
<div class="flex flex-col gap-1">
<CheckboxLabel class="text-sm leading-none">
{permission.description}
</CheckboxLabel>
</div>
</Checkbox>
)}
</For>
const sectionPermissions: ReadonlyArray<string> = section.permissions;
const currentPermissions = permissions();
const allSelected = sectionPermissions.every(p => currentPermissions.includes(p));
setPermissions((prev) => {
if (allSelected) {
return [...prev.filter(p => !sectionPermissions.includes(p))];
}
return [...new Set([...prev, ...sectionPermissions])];
});
};
return (
<div class="p-6 pb-8 border rounded-md mt-2">
<div class="flex flex-col gap-6">
<For each={getPermissionsSections()}>
{section => (
<div>
<Button variant="link" class="text-muted-foreground text-xs p-0 h-auto hover:no-underline" onClick={() => toggleSection(section.section)}>{section.title}</Button>
<div class="pl-4 flex flex-col mt-2">
<For each={section.permissions}>
{permission => (
<Checkbox
class="flex items-center gap-2"
checked={isPermissionSelected(permission.name)}
onChange={() => togglePermission(permission.name)}
>
<CheckboxControl />
<div class="flex flex-col gap-1">
<CheckboxLabel class="text-sm leading-none py-1">
{permission.description}
</CheckboxLabel>
</div>
</Checkbox>
)}
</For>
</div>
</div>
</div>
)}
</For>
)}
</For>
</div>
<div class="flex items-center gap-2 mt-6 border-t pt-6">
<Button
variant="outline"
size="sm"
class="disabled:(op-100! border-op-50 text-muted-foreground)"
onClick={() => setPermissions(API_KEY_PERMISSIONS.flatMap(section => section.permissions))}
disabled={permissions().length === API_KEY_PERMISSIONS.flatMap(section => section.permissions).length}
>
{t('api-keys.permissions.select-all')}
</Button>
<Button
variant="outline"
size="sm"
class="disabled:(op-100! border-op-50 text-muted-foreground)"
onClick={() => setPermissions([])}
disabled={permissions().length === 0}
>
{t('api-keys.permissions.deselect-all')}
</Button>
</div>
</div>
);
};

View File

@@ -96,9 +96,7 @@ export const CreateApiKeyPage: Component = () => {
<div>
<p class="text-sm font-bold">{t('api-keys.create.form.permissions.label')}</p>
<div class="p-6 pb-8 border rounded-md mt-2">
<ApiKeyPermissionsPicker permissions={field.value ?? []} onChange={permissions => setValue(form, 'permissions', permissions)} />
</div>
<ApiKeyPermissionsPicker permissions={field.value ?? []} onChange={permissions => setValue(form, 'permissions', permissions)} />
{field.error && <div class="text-red-500 text-sm">{field.error}</div>}
</div>

View File

@@ -1,5 +1,15 @@
# @papra/app-server
## 0.9.4
### Patch Changes
- [#508](https://github.com/papra-hq/papra/pull/508) [`782f70f`](https://github.com/papra-hq/papra/commit/782f70ff663634bf9ff7218edabb9885a7c6f965) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added an option to disable PRAGMA statements from sqlite task service migrations
- [#510](https://github.com/papra-hq/papra/pull/510) [`ab6fd6a`](https://github.com/papra-hq/papra/commit/ab6fd6ad10387f1dcd626936efc195d9d58d40ec) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added fallbacks env variables for the task worker id
- [#512](https://github.com/papra-hq/papra/pull/512) [`cb3ce6b`](https://github.com/papra-hq/papra/commit/cb3ce6b1d8d5dba09cbf0d2964f14b1c93220571) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added organizations permissions for api keys
## 0.9.3
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@papra/app-server",
"type": "module",
"version": "0.9.3",
"version": "0.9.4",
"private": true,
"packageManager": "pnpm@10.12.3",
"description": "Papra app server",
@@ -42,7 +42,7 @@
"@aws-sdk/lib-storage": "^3.835.0",
"@azure/storage-blob": "^12.27.0",
"@cadence-mq/core": "^0.2.1",
"@cadence-mq/driver-libsql": "^0.2.1",
"@cadence-mq/driver-libsql": "^0.2.4",
"@cadence-mq/driver-memory": "^0.2.0",
"@corentinth/chisels": "^1.3.1",
"@corentinth/friendly-ids": "^0.0.1",
@@ -63,7 +63,7 @@
"date-fns": "^4.1.0",
"drizzle-kit": "^0.30.6",
"drizzle-orm": "^0.38.4",
"figue": "^2.2.3",
"figue": "^3.1.1",
"hono": "^4.8.2",
"lodash-es": "^4.17.21",
"mime-types": "^3.0.1",

View File

@@ -5,8 +5,15 @@ export const API_KEY_ID_REGEX = createPrefixedIdRegex({ prefix: API_KEY_ID_PREFI
export const API_KEY_PREFIX = 'ppapi';
export const API_KEY_TOKEN_LENGTH = 64;
export const API_KEY_TOKEN_REGEX = new RegExp(`^${API_KEY_PREFIX}_[A-Za-z0-9]{${API_KEY_TOKEN_LENGTH}}$`);
export const API_KEY_PERMISSIONS = {
ORGANIZATIONS: {
CREATE: 'organizations:create',
READ: 'organizations:read',
UPDATE: 'organizations:update',
DELETE: 'organizations:delete',
},
DOCUMENTS: {
CREATE: 'documents:create',
READ: 'documents:read',

View File

@@ -4,6 +4,7 @@ import { createMiddleware } from 'hono/factory';
import { createUnauthorizedError } from '../app/auth/auth.errors';
import { getAuthorizationHeader } from '../shared/headers/headers.models';
import { isNil } from '../shared/utils';
import { looksLikeAnApiKey } from './api-keys.models';
import { createApiKeysRepository } from './api-keys.repository';
import { getApiKey } from './api-keys.usecases';
@@ -31,8 +32,7 @@ export function createApiKeyMiddleware({ db }: { db: Database }) {
throw createUnauthorizedError();
}
if (isNil(token)) {
// For type safety
if (!looksLikeAnApiKey(token)) {
throw createUnauthorizedError();
}

View File

@@ -1,5 +1,6 @@
import { describe, expect, test } from 'vitest';
import { getApiKeyUiPrefix } from './api-keys.models';
import { getApiKeyUiPrefix, looksLikeAnApiKey } from './api-keys.models';
import { generateApiToken } from './api-keys.services';
describe('api-keys models', () => {
describe('getApiKeyUiPrefix', () => {
@@ -11,4 +12,39 @@ describe('api-keys models', () => {
);
});
});
describe('looksLikeAnApiKey', () => {
test(`validate that a token looks like an api key
- it starts with the api key prefix
- it has the correct length
- it only contains alphanumeric characters`, () => {
expect(
looksLikeAnApiKey('ppapi_29qxv9eCbRkQQGhwrVZCEXEFjOYpXZX07G4vDK4HT03Jp7fVHyJx1b0l6e1LIEPD'),
).toBe(true);
expect(
looksLikeAnApiKey(''),
).toBe(false);
expect(
looksLikeAnApiKey('ppapi_'),
).toBe(false);
expect(
looksLikeAnApiKey('ppapi_29qxv9eCbRkQQGhwrVZCEXEFjOYpXZX07G4vDK4HT03Jp7fVHyJx1b0l6e1LIEPD_extra'),
).toBe(false);
expect(
looksLikeAnApiKey('invalidprefix_29qxv9eCbRkQQGhwrVZCEXEFjOYpXZX07G4vDK4HT03Jp7fVHyJx1b0l6e1LIEPD'),
).toBe(false);
});
test('a freshly generated token should always look like an api key', () => {
const { token } = generateApiToken();
expect(
looksLikeAnApiKey(token),
).toBe(true);
});
});
});

View File

@@ -1,5 +1,6 @@
import { sha256 } from '../shared/crypto/hash';
import { API_KEY_PREFIX } from './api-keys.constants';
import { isNil } from '../shared/utils';
import { API_KEY_PREFIX, API_KEY_TOKEN_REGEX } from './api-keys.constants';
export function getApiKeyUiPrefix({ token }: { token: string }) {
return {
@@ -12,3 +13,12 @@ export function getApiKeyHash({ token }: { token: string }) {
keyHash: sha256(token, { digest: 'base64url' }),
};
}
// Positional argument as TS does not like named argument with type guards
export function looksLikeAnApiKey(token?: string | null | undefined): token is string {
if (isNil(token)) {
return false;
}
return API_KEY_TOKEN_REGEX.test(token);
}

View File

@@ -27,7 +27,7 @@ export function registerOrganizationsRoutes(context: RouteDefinitionContext) {
function setupGetOrganizationsRoute({ app, db }: RouteDefinitionContext) {
app.get(
'/api/organizations',
requireAuthentication(),
requireAuthentication({ apiKeyPermissions: ['organizations:read'] }),
async (context) => {
const { userId } = getUser({ context });
@@ -45,7 +45,7 @@ function setupGetOrganizationsRoute({ app, db }: RouteDefinitionContext) {
function setupCreateOrganizationRoute({ app, db, config }: RouteDefinitionContext) {
app.post(
'/api/organizations',
requireAuthentication(),
requireAuthentication({ apiKeyPermissions: ['organizations:create'] }),
validateJsonBody(z.object({
name: z.string().min(3).max(50),
})),
@@ -70,7 +70,7 @@ function setupCreateOrganizationRoute({ app, db, config }: RouteDefinitionContex
function setupGetOrganizationRoute({ app, db }: RouteDefinitionContext) {
app.get(
'/api/organizations/:organizationId',
requireAuthentication(),
requireAuthentication({ apiKeyPermissions: ['organizations:read'] }),
validateParams(z.object({
organizationId: organizationIdSchema,
})),
@@ -92,7 +92,7 @@ function setupGetOrganizationRoute({ app, db }: RouteDefinitionContext) {
function setupUpdateOrganizationRoute({ app, db }: RouteDefinitionContext) {
app.put(
'/api/organizations/:organizationId',
requireAuthentication(),
requireAuthentication({ apiKeyPermissions: ['organizations:update'] }),
validateJsonBody(z.object({
name: z.string().min(3).max(50),
})),
@@ -120,7 +120,7 @@ function setupUpdateOrganizationRoute({ app, db }: RouteDefinitionContext) {
function setupDeleteOrganizationRoute({ app, db }: RouteDefinitionContext) {
app.delete(
'/api/organizations/:organizationId',
requireAuthentication(),
requireAuthentication({ apiKeyPermissions: ['organizations:delete'] }),
validateParams(z.object({
organizationId: organizationIdSchema,
})),

View File

@@ -1,9 +1,13 @@
import type { TaskPersistenceConfig, TaskServiceDriverDefinition } from '../../tasks.types';
import { createLibSqlDriver, setupSchema } from '@cadence-mq/driver-libsql';
import { safely } from '@corentinth/chisels';
import { createClient } from '@libsql/client';
import { createLogger } from '../../../shared/logger/logger';
const logger = createLogger({ namespace: 'tasks-driver:libsql' });
export function createLibSqlTaskServiceDriver({ taskPersistenceConfig }: { taskPersistenceConfig: TaskPersistenceConfig }): TaskServiceDriverDefinition {
const { url, authToken, pollIntervalMs } = taskPersistenceConfig.drivers.libSql;
const { url, authToken, pollIntervalMs, migrateWithPragma } = taskPersistenceConfig.drivers.libSql;
const client = createClient({ url, authToken });
const driver = createLibSqlDriver({ client, pollIntervalMs });
@@ -11,7 +15,15 @@ export function createLibSqlTaskServiceDriver({ taskPersistenceConfig }: { taskP
return {
driver,
initialize: async () => {
await setupSchema({ client });
logger.debug('Initializing LibSQL task service driver');
const [, error] = await safely(setupSchema({ client, withPragma: migrateWithPragma }));
if (error) {
logger.error({ error }, 'Failed to set up LibSQL task service schema');
throw error;
}
logger.info('LibSQL task service driver initialized');
},
};
}

View File

@@ -26,6 +26,12 @@ export const tasksConfig = {
default: undefined,
env: 'TASKS_PERSISTENCE_DRIVERS_LIBSQL_AUTH_TOKEN',
},
migrateWithPragma: {
doc: 'Whether to include the PRAGMA statements when setting up the LibSQL database schema.',
schema: booleanishSchema,
default: true,
env: 'TASKS_PERSISTENCE_DRIVERS_LIBSQL_MIGRATE_WITH_PRAGMA',
},
pollIntervalMs: {
doc: 'The interval at which the task persistence driver polls for new tasks',
schema: z.coerce.number().int().positive(),
@@ -39,7 +45,7 @@ export const tasksConfig = {
id: {
doc: 'The id of the task worker, used to identify the worker in the Cadence cluster in case of multiple workers',
schema: z.string().optional(),
env: 'TASKS_WORKER_ID',
env: ['TASKS_WORKER_ID', 'DYNO', 'RENDER_SERVICE_ID'],
},
},
hardDeleteExpiredDocuments: {

View File

@@ -48,6 +48,9 @@ ENV PAPRA_CONFIG_DIR=./app-data
ENV EMAILS_DRY_RUN=true
ENV CLIENT_BASE_URL=http://localhost:1221
# Disable Better Auth telemetry
ENV BETTER_AUTH_TELEMETRY=0
RUN mkdir -p ./app-data/db ./app-data/documents ./ingestion
CMD ["pnpm", "start:with-migrations"]

View File

@@ -61,4 +61,7 @@ ENV INGESTION_FOLDER_ROOT=./ingestion
ENV EMAILS_DRY_RUN=true
ENV CLIENT_BASE_URL=http://localhost:1221
# Disable Better Auth telemetry
ENV BETTER_AUTH_TELEMETRY=0
CMD ["pnpm", "start:with-migrations"]

40
pnpm-lock.yaml generated
View File

@@ -100,8 +100,8 @@ importers:
specifier: ^1.3.1
version: 1.3.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)
figue:
specifier: ^2.2.2
version: 2.2.3(zod@3.25.67)
specifier: ^3.1.1
version: 3.1.1
lodash-es:
specifier: ^4.17.21
version: 4.17.21
@@ -254,8 +254,8 @@ importers:
specifier: ^0.2.1
version: 0.2.1
'@cadence-mq/driver-libsql':
specifier: ^0.2.1
version: 0.2.1(@cadence-mq/core@0.2.1)(@libsql/client@0.14.0)
specifier: ^0.2.4
version: 0.2.4(@cadence-mq/core@0.2.1)(@libsql/client@0.14.0)
'@cadence-mq/driver-memory':
specifier: ^0.2.0
version: 0.2.0(@cadence-mq/core@0.2.1)
@@ -317,8 +317,8 @@ importers:
specifier: ^0.38.4
version: 0.38.4(@libsql/client@0.14.0)(kysely@0.28.2)(react@18.3.1)
figue:
specifier: ^2.2.3
version: 2.2.3(zod@3.25.67)
specifier: ^3.1.1
version: 3.1.1
hono:
specifier: ^4.8.2
version: 4.8.2
@@ -1076,10 +1076,10 @@ packages:
'@cadence-mq/core@0.2.1':
resolution: {integrity: sha512-Cu/jqR7mNhMZ1U4Boiudy2nePyf4PtqBUFGhUcsCQPJfymKcrDm4xjp8A/2tKZr5JSgkN/7L0/+mHZ27GVSryQ==}
'@cadence-mq/driver-libsql@0.2.1':
resolution: {integrity: sha512-tQPmMNLLVEhvT2HdY/rHk+Cl0Yj4JFMQnoYnBYIw30kTIpKGCQWnBTf5oSmIlmc6wdIHYan+f+waVhWmkObD1w==}
'@cadence-mq/driver-libsql@0.2.4':
resolution: {integrity: sha512-JXsajpPXJRQolYiPzYI5rpQyTjH1g7AZMh3KYnHHs8nieLekYhU885iRPCu80RXQsYN2CJa08Vj5hgQhGP9rjw==}
peerDependencies:
'@cadence-mq/core': ^0.2.0
'@cadence-mq/core': ^0.2.3
'@libsql/client': ^0.15.9
'@cadence-mq/driver-memory@0.2.0':
@@ -4654,8 +4654,8 @@ packages:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'}
detect-libc@2.0.4:
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
detect-libc@2.1.0:
resolution: {integrity: sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==}
engines: {node: '>=8'}
deterministic-object-hash@2.0.2:
@@ -5411,10 +5411,8 @@ packages:
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
figue@2.2.3:
resolution: {integrity: sha512-Dwj/Na3KuiGeFhiM2AieEcMhgXDbHfdWrRcSGdfAspeWTEC8OXleY/Mf+yty4YIK4gxpgQcpKhQm84XTMvXUqg==}
peerDependencies:
zod: ^3.22.4
figue@3.1.1:
resolution: {integrity: sha512-LYqDGKztWh0tU5eCTzW5nK3MAp4K5ihEsnyWiNwU6jW0yVle9DqQCiMra5lMTXE21d9x882ZoOK4SitMXvaQ3Q==}
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
@@ -9515,7 +9513,7 @@ snapshots:
'@standard-schema/spec': 1.0.0
cron-parser: 5.3.0
'@cadence-mq/driver-libsql@0.2.1(@cadence-mq/core@0.2.1)(@libsql/client@0.14.0)':
'@cadence-mq/driver-libsql@0.2.4(@cadence-mq/core@0.2.1)(@libsql/client@0.14.0)':
dependencies:
'@cadence-mq/core': 0.2.1
'@libsql/client': 0.14.0
@@ -10752,7 +10750,7 @@ snapshots:
'@mapbox/node-pre-gyp@1.0.11':
dependencies:
detect-libc: 2.0.4
detect-libc: 2.1.0
https-proxy-agent: 5.0.1
make-dir: 3.1.0
node-fetch: 2.7.0
@@ -13308,7 +13306,7 @@ snapshots:
detect-libc@2.0.3: {}
detect-libc@2.0.4:
detect-libc@2.1.0:
optional: true
deterministic-object-hash@2.0.2:
@@ -14356,9 +14354,9 @@ snapshots:
fflate@0.4.8: {}
figue@2.2.3(zod@3.25.67):
figue@3.1.1:
dependencies:
zod: 3.25.67
'@standard-schema/spec': 1.0.0
file-entry-cache@8.0.0:
dependencies:
@@ -17785,7 +17783,7 @@ snapshots:
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@5.4.19(@types/node@22.16.0))
'@vitest/mocker': 3.2.4(vite@5.4.19(@types/node@24.0.10))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4