@ceros-dev/markup-sdk-core
v1.0.0-rc.1
Published
Shared TypeScript types and utilities for MarkUp SDK packages
Readme
@ceros-dev/markup-sdk-core
Typed client, resource APIs, and shared types for the MarkUp SDK.
Stability. Follows semantic versioning from
1.0.0— no breaking changes to the public API within a major version. Published to public npm.For browser UI (commenting mode, pins, popovers) use
@ceros-dev/markup-sdk— this package is the headless layer underneath it.
Installation
npm install @ceros-dev/markup-sdk-coreQuickstart
import {ApiClient, StaticTokenAdapter} from "@ceros-dev/markup-sdk-core";
const client = new ApiClient({
baseUrl: "https://api.markup.io",
publicKey: process.env.MARKUP_PUBLIC_KEY,
authAdapter: new StaticTokenAdapter(process.env.MARKUP_TOKEN, {workspaceId: "ws_123"})
});
const threads = await client.threads.listOpen("project_abc");
for (const thread of threads.data) {
console.log(thread.id, thread.priority, thread.createdAt);
}ApiClient is the single entry point. It wires an internal HttpClient (retries, timeouts,
idempotency keys, rate-limit headers) to typed resource accessors. Consumers should always use
client.threads, client.comments, etc. — do not construct resource classes or HttpClient
directly.
HttpClient is not exported from the main @ceros-dev/markup-sdk-core barrel. An
@ceros-dev/markup-sdk-core/internal subpath exists for sibling packages in the Ceros monorepo
(notably @ceros-dev/markup-sdk's AuthManager); it is @internal and not covered by the public
semver contract — downstream integrators must not import from it.
Authentication
Auth is handled by an adapter passed to the client at construction. The transport pulls
sessions from the adapter lazily, caches them until expiresAt, and transparently calls
getSession(true) on a 401 to refresh before retrying the request exactly once. Callers never
set tokens imperatively.
1. Static token
Use StaticTokenAdapter for scripts or tests that already hold a token:
import {StaticTokenAdapter} from "@ceros-dev/markup-sdk-core";
const adapter = new StaticTokenAdapter("eyJhbGciOi...", {workspaceId: "ws_123"});2. Custom adapter (token refresh)
Implement IAuthAdapter to plug in a refresh strategy:
import type {IAuthAdapter, AuthSession} from "@ceros-dev/markup-sdk-core";
class MyAuthAdapter implements IAuthAdapter {
private cached: AuthSession | null = null;
async getSession(forceRefresh?: boolean): Promise<AuthSession> {
if (!forceRefresh && this.cached && this.cached.expiresAt! > Date.now()) {
return this.cached;
}
const res = await fetch("/api/markup-token");
const {accessToken, workspaceId, expiresAt} = await res.json();
this.cached = {accessToken, workspaceId, expiresAt};
return this.cached;
}
clear() {
this.cached = null;
}
}
const client = new ApiClient({
baseUrl: "https://api.markup.io",
publicKey: process.env.MARKUP_PUBLIC_KEY,
authAdapter: new MyAuthAdapter()
});AuthSession carries {accessToken, workspaceId, expiresAt?}. The transport handles refresh
automatically — implementations should cache internally and only hit the wire when
forceRefresh === true or the cached session is missing/expired.
Error handling
Every HTTP error thrown by the client is a MarkUpError subclass. Narrow with instanceof
and read code, status, requestId, and retryable for structured recovery.
import {
ApiClient,
MarkUpError,
AuthenticationError,
NotFoundError,
RateLimitError,
ValidationError,
SDKErrorCode
} from "@ceros-dev/markup-sdk-core";
try {
await client.threads.create(request);
} catch (err) {
if (err instanceof ValidationError) {
for (const fieldErr of err.errors) {
console.warn(`${fieldErr.field}: ${fieldErr.message}`);
}
} else if (err instanceof AuthenticationError) {
// Transport already attempted one refresh+retry. A persistent 401 means the adapter
// cannot produce a valid token — e.g. the refresh token has expired.
console.error("auth failed", err.requestId);
} else if (err instanceof RateLimitError) {
await new Promise((r) => setTimeout(r, err.retryAfter * 1000));
} else if (err instanceof NotFoundError) {
console.warn(`Missing ${err.resourceType}: ${err.resourceId}`);
} else if (err instanceof MarkUpError) {
console.error({
code: err.code,
status: err.status,
requestId: err.requestId,
retryable: err.retryable
});
} else {
throw err;
}
}MarkUpError subclasses include ApiError, AuthenticationError, NotFoundError,
ValidationError, RateLimitError, TimeoutError, and NetworkError. All carry the
fields above plus sdkVersion and a toJSON() serializer for telemetry.
Two helpers are exported for code that can't rely on instanceof (e.g. across realms):
isMarkUpError(err) is a type guard, and canRetryError(err) returns true for retryable
MarkUpErrors (and for a raw TypeError, treated as a network failure).
The SDKErrorCode enum also defines browser-only sign-in codes — POPUP_BLOCKED,
POPUP_CLOSED, WORKSPACE_FORBIDDEN, and SDK_SIGNIN_FAILED — surfaced by the SDK-managed
sign-in flow in @ceros-dev/markup-sdk.
Resource APIs
Five resource groups are exposed on ApiClient:
| Accessor | Operations |
| ------------------ | ------------------------------------------------------------------- |
| client.projects | get, getTagData, setReadOnly |
| client.threads | list, listOpen, listResolved, listByProject, get, create, resolve, unresolve, delete, movePin |
| client.comments | list, listByThread, create, update, delete |
| client.viewModes | list |
| client.uploads | getUploadPolicy, completeUpload, directUpload |
Projects
const {data: project} = await client.projects.get("project_abc");
const {data: tagData} = await client.projects.getTagData("project_abc");
await client.projects.setReadOnly("project_abc", true);Threads
const open = await client.threads.listOpen("project_abc", {limit: 50});
const page2 = await client.threads.listOpen("project_abc", {cursor: open.meta.cursor});
const {data: thread} = await client.threads.get("thread_123");
await client.threads.resolve("thread_123");
await client.threads.delete("thread_123");threads.list({status: "all"}) merges two parallel requests. Cursor pagination is not
supported in that mode — pass "open" or "resolved" for paginated results.
Comments
const {data: comments} = await client.comments.listByThread("thread_123");
await client.comments.create({
threadId: "thread_123",
content: "Looks good — shipping.",
mentionIds: ["user_456"]
});
await client.comments.update("message_789", {content: "Looks good — shipping Monday."});
await client.comments.delete("message_789");View modes
const viewModes = await client.viewModes.list("project_abc");Uploads
Two flows depending on file size:
import {UploadResourceType} from "@ceros-dev/markup-sdk-core";
// Small files (up to 20MB): direct multipart upload
const fd = new FormData();
fd.append("file", file);
const {data: attachment} = await client.uploads.directUpload(fd);
// Large files: request a presigned policy, POST the file to S3, then confirm
const {data: policy} = await client.uploads.getUploadPolicy({
resourceType: UploadResourceType.MESSAGE_ATTACHMENT,
resourceId: "thread_123",
filename: file.name,
contentType: file.type,
filesize: file.size
});
// ...POST the file to policy.policy.url with policy.policy.fields, then capture the S3 ETag...
const {data: ref} = await client.uploads.completeUpload({
fileId: policy.fileId,
etag
});Subpath exports
Granular entry points for consumers that only need a subset of the package:
import {ApiClient, MarkUpError} from "@ceros-dev/markup-sdk-core/client";
import type {Thread, Comment, Project} from "@ceros-dev/markup-sdk-core/types";
import {API_PATHS} from "@ceros-dev/markup-sdk-core/api";
import {isValidUuid, formatRelativeTime} from "@ceros-dev/markup-sdk-core/utils";./client carries the runtime client + error types, ./types is type-only domain models, ./api
exposes the /api/v2 path and header constants, and ./utils carries validation and formatting
helpers. The package root (@ceros-dev/markup-sdk-core) re-exports ./client, ./types, and
./api; the ./utils helpers are available only from the ./utils subpath.
Stability
- Versioning: Semantic versioning from
1.0.0— no breaking changes to the public API within a major version. Published to public npm. - Client header: Every request sends
markup-client: sdk/<clientName>/<version>for telemetry.clientNamedefaults tocorebut can be overridden via theclientNameconstructor option (e.g.@ceros-dev/markup-sdksets its own).
License
BSD 3-Clause — see LICENSE.
