openvibers/openvibe.sdk Commits

Add the Pics gallery, uploads and albums to the SDK (#169)

OP OpenVibers committed 4 days ago · 3bedba2063 · 17 files, +136 −17

* Add the Pics gallery, uploads and albums to the SDK

* Merge main into the Pics SDK branch, as 0.124.0

* Merge current main into the Pics SDK branch

Keeps openvibe-sdk/pics on top of the Link rename and 0.124.0.

* Pics SDK 0.125.0: merge main's Link release, and take the next free version

main released 0.124.0 for OpenVibe Link before this branch landed, so the
Pics subpath moves to 0.125.0 and its CHANGELOG entry sits above Link's.
The pack test still expects 99 subpaths, and STATUS.json lists the pics
subpath without touching main's other entries.

* Merge main and release the Pics SDK as 0.126.0

Main released 0.125.0 with the blog block client while this branch was in review, so
openvibe-sdk/pics moves to 0.126.0 above it. Keeps the Link rename and blog entries.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 4b6f4f5..444baa1 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,9 @@
# Changelog
++## 0.126.0 (2026-11-09)
++
++- Add `openvibe-sdk/pics`: the public gallery and picture reads, private uploads with resumable chunks, publication, likes and albums on OpenVibe.Pics.
++
## 0.125.0 (2026-11-09)
- openvibe-sdk/blog: posts are written and read as Markdown or as blocks (publishing.block@1), ?format=blocks
ddiff --git a/README.md b/README.md
iindex 9152ddf..9251f80 100644
---- a/README.md
++++ b/README.md
@@@ -10,7 +10,7 @@ npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags
> Supported browser and server clients for the OpenVibe platform.
--**Status:** alpha, v0.125.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
++**Status:** alpha, v0.126.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.
**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.
@@@ -19,7 +19,7 @@ npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags
After v0.97.1 is tagged:
```bash
--npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.125.0
++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.126.0
```
It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from the temporary Contracts sharing branch, a devDependency the tests check them against).
@@@ -693,3 +693,20 @@ await exchange.arena.vote(round.id, 'left');
Camera choices can be read or saved on behalf of a signed-in person with `ov.cam.settings.get({ token })` and
`ov.cam.settings.put({ token, values: { facing, lens, mic, resolution, mirror } })`. Server callers should forward
the person's Network token; Cam keeps these preferences per person, and a missing device id is only a hint.
++
++### Pictures
++
++```js
++const { createPicsClient } = require('openvibe-sdk/pics');
++const pics = createPicsClient(client);
++const gallery = await pics.gallery({ sort: 'new', tag: 'landscape' });
++const session = await pics.uploads.create({ size: bytes.length, content_type: 'image/png', name: 'photo.png' });
++// Send at most 1 MiB per chunk; read uploads.get(id).offset after an interrupted request.
++await pics.uploads.chunk(session.id, 0, bytes);
++const picture = await pics.uploads.complete(session.id); // private
++await pics.publish(picture.slug, { listed: true, tags: ['landscape'] });
++```
++
++`pics.albums` lists, creates and manages albums; `pics.like(slug, false)` removes a
++like. Upload and write calls require a person's Network token. Gallery reads are
++public. Resumable completion and likes are idempotent; creation is not retried.
ddiff --git a/STATUS.json b/STATUS.json
iindex 39ef9cd..8cea3e0 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -3,17 +3,18 @@
"stage": "alpha",
"code": true,
"wave": "W2",
-- "version": "0.125.0",
-- "plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
++ "version": "0.126.0",
++ "plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), \u00a73.2; roadmap \u00a730",
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
"contracts": "openvibe-contracts v0.338.0",
"features": [
"menu (openvibe-sdk/menu): menuCard() builds one validated frame.menu-card@1 (plain text, the contract caps, https links on the site own origin or an openvibe.* host, an optional item picture) and menuRoute() is the Express/node:http GET /.well-known/openvibe-menu.json handler (public cache, CORS *, [] and one log line when the cards cannot be built)",
"cam (openvibe-sdk/cam): defineOvCamera({ src }) injects the <ov-camera> element from openvibe.cam once and resolves to its class, with typed attributes, events and methods (start/stop/capture/flip); browser-only, upload=\"off\" by default",
++ "pics (openvibe-sdk/pics): the public gallery and one picture's reads, private uploads with resumable chunks, the owner's explicit publication, likes and albums on OpenVibe.Pics; reads are public, writes take a person's Network token",
"streams (openvibe-sdk/streams, S7): schedule(), listSchedules(), cancelSchedule() and remindSchedule() for scheduled public agent streams; postStep() and steps.post() from any harness; digest.read() and poll.open()/poll.result() for a Co-pilot service token; onControl() for pause, curtain, mute and end",
"reports (openvibe-sdk/reports, S7): write() a session report with the person's own token, private by default and idempotent per source and task ref, and read() one the caller may see; examples/cli-agent-stream.js streams any CLI command's run with steps",
-- "chat-apps (openvibe-sdk/chat-apps): the three chat apps (Telegram, Discord, Slack) in one place — the delivery checks (Telegram's secret token, Slack's v0 signing, Discord's Ed25519), the button callback (signed once, used once, expiring), each app's payload as the same shape and splitText() for a long answer",
++ "chat-apps (openvibe-sdk/chat-apps): the three chat apps (Telegram, Discord, Slack) in one place \u2014 the delivery checks (Telegram's secret token, Slack's v0 signing, Discord's Ed25519), the button callback (signed once, used once, expiring), each app's payload as the same shape and splitText() for a long answer",
"exchange (openvibe-sdk/exchange): the buyer side (chat, models, offers, presets, usage, receipts) and the seller side (offer, endpointOffers, removeEndpointOffer, earnings, verification) of OpenVibe.Exchange, with the platform webhook signature check; an offer is a Link machine's Network report or a standalone OpenAI-compatible endpoint registered on the public seller API",
"locker (openvibe-sdk/locker, openvibe-sdk/locker/server): the Locker envelope (locker.envelope@1) for end-to-end encryption, WebCrypto only, in Node and the browser: seal/open for text and bytes, sealStream/openStream, the three locks (people, password, link), person key pairs (ECDH P-256, ECDSA P-256), the eight-word fingerprint, wrapFor and rewrap, recoveryWords for the phrase to write down, opening a people lock by key, inspect for what an envelope says about itself, and the header checks a service runs without a key",
"the 1.0 core surface is frozen now, before 1.0 (core, auth, events, db, media): docs/one-point-oh.json gives every export its tier, test/surface.test.js pins the exact names, and removing or renaming one needs a major release; createCodeVerifier and pkceChallenge left in 0.86.0",
@@@ -37,7 +38,7 @@
"valkey, cache, queue, pubsub and a shared limits store on Valkey 9, each with an in-process twin for tests",
"commerce (openvibe-sdk/commerce): the shared Billing client for Tips and VIP, with intentKey and receipt",
"events: the PostgreSQL outbox (lease-claimed, several relays per table) and inbox (handler in its transaction)",
-- "usage: createUsageReporter (openvibe-sdk/usage), the shared platform.usage-sample@1 reporter — validated readings queued idempotency-keyed in an outbox and relayed to Billing with retry, never dropped, refused readings kept",
++ "usage: createUsageReporter (openvibe-sdk/usage), the shared platform.usage-sample@1 reporter \u2014 validated readings queued idempotency-keyed in an outbox and relayed to Billing with retry, never dropped, refused readings kept",
"the HTTP telemetry kit (openvibe-sdk/service): the platform.telemetry-sample@1 middleware aggregated per route|method|status_class per flush, with active-requests, p95 and event-loop-lag gauges, lifted from Network's server/telemetry.js and server/observability.js",
"the telemetry sink (openvibe-sdk/telemetry): telemetrySchema() DDL, createPgTelemetrySink() and pruneTelemetry(), a per-service PostgreSQL table every service stores platform.telemetry-sample@1 into",
"account-data (openvibe-sdk/account-data, ADR-033): telemetry_samples covered once by the SDK (exported as telemetry.json, anonymized on deletion), and uncovered() listing every table with a person column that no table map, task-keyed list or reasoned exemption covers",
@@@ -46,8 +47,8 @@
"the AI Exchange face of the one planner (openvibe-sdk/placement planAi, off by default): the hard gates (exact artifact, commercial right, privacy class, trust tier, capabilities, context, health, cost ceiling), the expected cost per success in integer nano-USD, sparse quality evidence marked and not trusted, and an explanation that never carries a secret or a host; the money math in openvibe-sdk/cost (nanoUsd, chargeTokens, expectedCostNano, costPerSuccessNano)",
"consent (openvibe-sdk/consent): the L24 check before any use of a person's content or likeness for AI or data; allowed() and filter() (batches of 500), onChange() drops a subject's answers on network.consent.changed, mock() for tests; fails closed, caches 60 s, server-side",
"openvibe-sdk/machines: the network's machines hub (list, get, catalogue, pair, rename, roles, revoke) against OpenVibe.Network, for a person and for the apps they consented to",
-- "vibes (openvibe-sdk/vibes): Vibes Pay charges (create with the app reference as the Idempotency-Key, get, refund) and constant-time webhook verification (X-OpenVibe-Signature-V2, ±300 s) for developer apps",
-- "streams (openvibe-sdk/streams): the Agent Streams plan S1 — start an agent stream on OpenVibe.Live (live.agent-stream@1) with its publish ticket, renew and revoke that ticket (the kill switch), poll state, post the last frame, set the curtain, switch visibility, end it and mint the view ticket; the publish ticket is a bearer of its own on the desk routes"
++ "vibes (openvibe-sdk/vibes): Vibes Pay charges (create with the app reference as the Idempotency-Key, get, refund) and constant-time webhook verification (X-OpenVibe-Signature-V2, \u00b1300 s) for developer apps",
++ "streams (openvibe-sdk/streams): the Agent Streams plan S1 \u2014 start an agent stream on OpenVibe.Live (live.agent-stream@1) with its publish ticket, renew and revoke that ticket (the kill switch), poll state, post the last frame, set the curtain, switch visibility, end it and mint the view ticket; the publish ticket is a bearer of its own on the desk routes"
],
"updated": "2026-11-09"
}
ddiff --git a/browser/openvibe-sdk.mjs b/browser/openvibe-sdk.mjs
iindex ab292e7..58ab6b0 100644
---- a/browser/openvibe-sdk.mjs
++++ b/browser/openvibe-sdk.mjs
@@@ -1,4 +1,4 @@
--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.125.0, MIT). Do not edit.
++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.126.0, MIT). Do not edit.
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// (auth, locker, registry, trust, modules, realtime, media, cam, mediaUpload, ui, community, homeFeed, bot, search, bench, meme, vision, themes, design, redact, chat, ai, credentials, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
@@@ -56,7 +56,7 @@ const { paginate, offsetPager } = __load(7);
const semver = __load(6);
/** The package version (test/version.test.js keeps it equal to package.json). */
--const SDK_VERSION = '0.125.0';
++const SDK_VERSION = '0.126.0';
module.exports = {
createClient,
ddiff --git a/docs/api-surface.json b/docs/api-surface.json
iindex c15dd4b..346c575 100644
---- a/docs/api-surface.json
++++ b/docs/api-surface.json
@@@ -282,6 +282,11 @@
},
"paginate": "function",
"parseTraceparent": "function",
++ "pics": {
++ "object": [
++ "createPicsClient"
++ ]
++ },
"play": {
"object": [
"createPlayClient"
@@@ -930,6 +935,9 @@
"openvibe-sdk/openre": {
"createOpenReClient": "function"
},
++ "openvibe-sdk/pics": {
++ "createPicsClient": "function"
++ },
"openvibe-sdk/placement": {
"DEFAULT_P_FLOOR": "number",
"DEFAULT_TRUST": "array",
ddiff --git a/esm/index.mjs b/esm/index.mjs
iindex 2054a47..a61bdf5 100644
---- a/esm/index.mjs
++++ b/esm/index.mjs
@@@ -13,7 +13,8 @@ export const {
projects, resources, vip, quest, live, inventory,
tips, blog, reviews, wiki, coupons, deals,
shoes, trade, space, host, news, food,
-- actor, run, video, work, zone, play,
-- download, openre, geo, usage, commerce, telemetry,
++ actor, run, video, pics, work, zone,
++ play, download, openre, geo, usage, commerce,
++ telemetry,
} = cjs;
export default cjs;
ddiff --git a/esm/pics.mjs b/esm/pics.mjs
nnew file mode 100644
iindex 0000000..36af36e
---- /dev/null
++++ b/esm/pics.mjs
@@@ -0,0 +1,7 @@
++// Generated by scripts/esm.js from src/pics.js. Do not edit.
++import cjs from '../src/pics.js';
++
++export const {
++ createPicsClient,
++} = cjs;
++export default cjs;
ddiff --git a/index.js b/index.js
iindex 0e8a15f..b22ae0a 100644
---- a/index.js
++++ b/index.js
@@@ -60,6 +60,7 @@ module.exports = {
actor: require('./src/actor'),
run: require('./src/run'),
video: require('./src/video'),
++ pics: require('./src/pics'),
work: require('./src/work'),
zone: require('./src/zone'),
play: require('./src/play'),
ddiff --git a/package-lock.json b/package-lock.json
iindex 2c943e3..a28cb42 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-sdk",
-- "version": "0.125.0",
++ "version": "0.126.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-sdk",
-- "version": "0.125.0",
++ "version": "0.126.0",
"license": "MIT",
"bin": {
"openvibe-download": "bin/openvibe-download.js"
ddiff --git a/package.json b/package.json
iindex 2522844..f2bcf4b 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-sdk",
-- "version": "0.125.0",
++ "version": "0.126.0",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"main": "index.js",
"types": "types/index.d.ts",
@@@ -409,6 +409,12 @@
"import": "./esm/video.mjs",
"default": "./src/video.js"
},
++ "./pics": {
++ "types": "./types/pics.d.ts",
++ "require": "./src/pics.js",
++ "import": "./esm/pics.mjs",
++ "default": "./src/pics.js"
++ },
"./work": {
"types": "./types/work.d.ts",
"browser": null,
ddiff --git a/scripts/esm.js b/scripts/esm.js
iindex ae2441f..9ab3043 100644
---- a/scripts/esm.js
++++ b/scripts/esm.js
@@@ -100,6 +100,7 @@ const ENTRIES = {
'actor.mjs': '../src/actor.js',
'run.mjs': '../src/run.js',
'video.mjs': '../src/video.js',
++ 'pics.mjs': '../src/pics.js',
'work.mjs': '../src/work.js',
'zone.mjs': '../src/zone.js',
'vip.mjs': '../src/vip.js',
ddiff --git a/src/core/index.js b/src/core/index.js
iindex 8495981..509e46c 100644
---- a/src/core/index.js
++++ b/src/core/index.js
@@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
const semver = require('./semver');
/** The package version (test/version.test.js keeps it equal to package.json). */
--const SDK_VERSION = '0.125.0';
++const SDK_VERSION = '0.126.0';
module.exports = {
createClient,
ddiff --git a/src/pics.js b/src/pics.js
nnew file mode 100644
iindex 0000000..f0f403d
---- /dev/null
++++ b/src/pics.js
@@@ -0,0 +1,35 @@
++'use strict';
++/** Pics discovery, private uploads, explicit publication and albums. */
++function createPicsClient(client, { baseUrl = 'https://openvibe.pics' } = {}) {
++ if (!client || typeof client.json !== 'function') throw new TypeError('createPicsClient requires an SDK core client');
++ const call = (o) => client.json({ service: 'media-hub', audience: 'openvibe.download', baseUrl, ...o });
++ const enc = (v) => encodeURIComponent(String(v));
++ const image = (slug) => `/api/v1/images/${enc(slug)}`;
++ const upload = (id) => `/api/v1/uploads/${enc(id)}`;
++ const album = (slug) => `/api/v1/albums/${enc(slug)}`;
++ return {
++ gallery: (query = {}) => call({ path: '/api/v1/gallery', query }),
++ get: (id) => call({ path: `/api/v1/pictures/${enc(id)}` }),
++ mine: (query = {}) => call({ path: '/api/v1/me/images', query }),
++ update: (slug, patch) => call({ method: 'PATCH', path: image(slug), json: patch, idempotent: true }),
++ publish: (slug, { listed, tags } = {}) => call({ method: 'POST', path: `${image(slug)}/publication`, json: { listed, tags }, idempotent: true }),
++ like: (slug, liked = true) => call({ method: liked ? 'PUT' : 'DELETE', path: `${image(slug)}/like`, idempotent: true }),
++ remove: (slug) => call({ method: 'DELETE', path: image(slug) }),
++ uploads: {
++ create: (input) => call({ method: 'POST', path: '/api/v1/uploads', json: input, idempotencyKey: false }),
++ get: (id) => call({ path: upload(id) }),
++ chunk: (id, offset, bytes) => call({ method: 'PATCH', path: upload(id), headers: { 'content-type': 'application/octet-stream', 'upload-offset': String(offset) }, body: bytes, idempotent: false, retry: false }),
++ complete: (id) => call({ method: 'POST', path: `${upload(id)}/complete`, idempotent: true }),
++ cancel: (id) => call({ method: 'DELETE', path: upload(id) }),
++ },
++ albums: {
++ list: () => call({ path: '/api/v1/albums' }),
++ create: (input) => call({ method: 'POST', path: '/api/v1/albums', json: input, idempotencyKey: false }),
++ get: (slug) => call({ path: album(slug) }),
++ add: (slug, pictureSlug) => call({ method: 'POST', path: `${album(slug)}/images`, json: { image: pictureSlug }, idempotent: true }),
++ removeImage: (slug, pictureSlug) => call({ method: 'DELETE', path: `${album(slug)}/images/${enc(pictureSlug)}` }),
++ remove: (slug) => call({ method: 'DELETE', path: album(slug) }),
++ },
++ };
++}
++module.exports = { createPicsClient };
ddiff --git a/test/pack.test.js b/test/pack.test.js
iindex 89c57ab..215e48b 100644
---- a/test/pack.test.js
++++ b/test/pack.test.js
@@@ -72,7 +72,7 @@ run([
})().catch((err) => { console.error(err); process.exit(1); });`;
const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });
assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));
-- assert.equal(subpaths.length, 98); // Adds the upload client and the UI entry (0.123.0); the wallet came in 0.119.0.
++ assert.equal(subpaths.length, 99); // Pics adds one subpath (0.125.0); the upload client and UI entry came in 0.123.0.
assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
ddiff --git a/test/pics.test.js b/test/pics.test.js
nnew file mode 100644
iindex 0000000..532bbea
---- /dev/null
++++ b/test/pics.test.js
@@@ -0,0 +1,21 @@
++'use strict';
++const assert = require('node:assert/strict');
++const { run, stubServer } = require('./helpers');
++const { createClient } = require('../src/core');
++const { createPicsClient } = require('../src/pics');
++run([[
++ 'Pics routes preserve discovery queries, person audience and raw chunk bytes', async () => {
++ const server = await stubServer(async (req, res, body) => { res.setHeader('content-type','application/json'); res.end(JSON.stringify({ ok:true })); });
++ try {
++ const pics = createPicsClient(createClient(), { baseUrl: server.url });
++ await pics.gallery({sort:'rising',tag:'forest',cursor:'30'});
++ await pics.uploads.chunk('pup_example', 7, Buffer.from([1,2,255]));
++ await pics.publish('Abcdef23',{listed:true,tags:['forest']});
++ await pics.like('Abcdef23',false);
++ const [g,c,p,l] = server.requests;
++ assert.match(g.url,/sort=rising/); assert.match(g.url,/cursor=30/);
++ assert.equal(c.method,'PATCH'); assert.equal(c.headers['upload-offset'],'7'); assert.deepEqual([...c.body],[1,2,255]);
++ assert.deepEqual(JSON.parse(p.body),{listed:true,tags:['forest']}); assert.equal(l.method,'DELETE');
++ } finally { await server.close(); }
++ }
++]]);
ddiff --git a/types/index.d.ts b/types/index.d.ts
iindex 9409bb6..0263fa6 100644
---- a/types/index.d.ts
++++ b/types/index.d.ts
@@@ -50,6 +50,7 @@ export * as food from './food';
export * as actor from './actor';
export * as run from './run';
export * as video from './video';
++export * as pics from './pics';
export * as work from './work';
export * as zone from './zone';
export * as play from './play';
ddiff --git a/types/pics.d.ts b/types/pics.d.ts
nnew file mode 100644
iindex 0000000..a1f5142
---- /dev/null
++++ b/types/pics.d.ts
@@@ -0,0 +1,16 @@
++export interface Picture { id: string; slug: string; title: string; description: string; width: number; height: number; listed: boolean; tags: string[]; links: { page: string; direct: string; thumb: string; markdown: string; html: string; bbcode: string }; likes?: number; views?: number; author?: string }
++export interface GalleryQuery { sort?: 'popular' | 'new' | 'rising'; q?: string; tag?: string; cursor?: string; limit?: number }
++export interface Upload { id: string; size: number; offset: number; expires_at: string; image_id: string | null }
++export interface Album { id: string; slug: string; title: string; description: string; images?: Picture[] }
++export interface PicsClient {
++ gallery(query?: GalleryQuery): Promise<{ pictures: Picture[]; next: string | null; sort: string }>;
++ get(id: string): Promise<Picture>;
++ mine(query?: { before?: string; limit?: number }): Promise<{ images: Picture[]; next: string | null }>;
++ update(slug: string, patch: { title?: string; description?: string; alt?: string }): Promise<Picture>;
++ publish(slug: string, input: { listed: boolean; tags?: string[] }): Promise<Picture>;
++ like(slug: string, liked?: boolean): Promise<{ likes: number; views: number }>;
++ remove(slug: string): Promise<void>;
++ uploads: { create(input: { size: number; content_type: string; name?: string; title?: string }): Promise<Upload>; get(id: string): Promise<Upload>; chunk(id: string, offset: number, bytes: Uint8Array): Promise<Upload>; complete(id: string): Promise<Picture>; cancel(id: string): Promise<void> };
++ albums: { list(): Promise<{ albums: Album[] }>; create(input: { title: string; description?: string }): Promise<Album>; get(slug: string): Promise<Album>; add(slug: string, pictureSlug: string): Promise<unknown>; removeImage(slug: string, pictureSlug: string): Promise<void>; remove(slug: string): Promise<void> };
++}
++export function createPicsClient(client: { json(options: Record<string, unknown>): Promise<any> }, options?: { baseUrl?: string }): PicsClient;