@cosmoner/sdk

中文文档
1.20.1发布于 2026/11/09MITTypeScriptESM + CJS构建来源已验证近 30 天下载 2174

Cosmoner SDK for Node.js

原始使用文档
文档目录19 节
  1. Installation
  2. Usage
  3. Configuration
  4. Retries
  5. Email
  6. client.email.send(params)
  7. Apps
  8. client.apps.deploy(appId, params?)
  9. client.apps.waitForDeployment(appId, deploymentId, params?)
  10. client.apps.listDeployments(appId, params?) and client.apps…
  11. Hosting
  12. Site image, PHP version and email
  13. Resource namespaces
  14. Ordering paid resources
  15. Secrets
  16. Variables
  17. Deployment files
  18. validateDeployment(source, options?)
  19. Error Handling

@cosmoner/sdk

Official Node.js SDK for Cosmoner, the cloud platform for apps, databases, storage, domains and email. Full documentation is at https://cosmoner.com/docs/sdk.

Installation

npm install @cosmoner/sdk

Usage

import { Cosmoner } from "@cosmoner/sdk";

const client = new Cosmoner({
  apiKey: "your-api-key",
  projectId: "your-project-id",
});

// Send to a single recipient
const result = await client.email.send({
  credentialId: "your-credential-id",
  to: "[email protected]",
  subject: "Hello from Cosmoner",
  html: "<h1>Hello!</h1><p>This is a test email.</p>",
});

console.log(result.data.messageId);

// Send to multiple recipients (up to 50)
await client.email.send({
  credentialId: "your-credential-id",
  to: ["[email protected]", "[email protected]"],
  subject: "Team Update",
  text: "Plain text email body",
  replyTo: "[email protected]",
});

Configuration

OptionRequiredDefaultDescription
apiKeyYes—Your Cosmoner API key
projectIdNo—Default project; can also be passed per call
baseUrlNohttps://api.cosmoner.comAPI base URL override
timeoutNo30000Per-request timeout in milliseconds
maxRetriesNo2Retries for transient failures (see Retries)

projectId is optional so one client can span projects. Pass it per call to override the default:

const client = new Cosmoner({ apiKey: "your-api-key" });

await client.email.send({ projectId: "proj-2", credentialId: "...", to: "...", subject: "...", text: "..." });

Retries

Transient failures are retried automatically with exponential backoff and full jitter, honouring Retry-After when the API sends it.

  • 429 is always retried — the request was rejected before it was processed, so replaying it cannot duplicate a side effect.
  • Timeouts and 5xx are retried only for idempotent methods. A write such as email.send is not replayed, because the API may have completed the send before failing to answer.

Set maxRetries: 0 to disable retries entirely.

Email

client.email.send(params)

Takes either raw content — a subject with html, text or both — or a stored template, which supplies all three. The two forms are a union type, so TypeScript refuses a call that mixes them, and the client rejects one at runtime before sending anything.

ParameterTypeRequiredDescription
credentialIdstringYesSMTP credential ID
tostring | string[]YesRecipient(s), max 50
subjectstringYes*Email subject line
htmlstringNo*HTML body
textstringNo*Plain text body
templateEmailTemplateRefNo*A stored template to send instead
replyTostring | string[]NoReply-to address(es), max 5
projectIdstringNoOverrides the client-level project

* Pass subject with at least one of html or text, or pass template and none of the three.

template is { id, version?, variables? }: id is the template's id or its alias, version a positive integer (the published version when left out), and variables the values for the variables the template declares. The response names what was rendered under data.template (id, alias, version).

const { data } = await client.email.send({
  credentialId: "your-credential-id",
  to: "[email protected]",
  template: { id: "welcome", variables: { name: "Ada" } },
});

console.log(data.messageId, data.template?.version);

Sending a template needs an API key with email-templates:read as well as email:write. The API answers 404 for an unknown template or version, 409 (EMAIL_TEMPLATE_NOT_PUBLISHED) when no version has been published, 422 when the variables do not match what the template declares or rendering hits a limit, and 503 when the template is briefly unavailable.

Apps

const { data: apps } = await client.apps.list();
const web = apps.find((app) => app.name === "web")!;

const { data: started } = await client.apps.deploy(web.id, { tag: "v2" });
const finished = await client.apps.waitForDeployment(web.id, started.id);

if (finished.phase !== "ACTIVE") throw new Error(finished.error ?? finished.phase);

client.apps.deploy(appId, params?)

Starts deploying an image app — one that runs an image from a Cosmoner registry — and returns the deployment without waiting. Apps built from a repository are refused. Needs an API key with apps:write.

ParameterTypeRequiredDescription
tagstringNoTag to deploy from the app's repository
digeststringNoExact image, sha256:<64 hex characters>
projectIdstringNoOverrides the client-level project

Pass tag or digest, not both. With neither, the image the app already names is pulled again.

client.apps.waitForDeployment(appId, deploymentId, params?)

Polls until the deployment finishes and resolves with it in its final phase — ACTIVE, ERROR, CANCELED or SUPERSEDED. It rejects only when a request fails or timeout (default 10 minutes) passes; the deployment carries on either way. interval defaults to 3 seconds, and onPoll is called with every result. Needs apps:read.

client.apps.list() and client.apps.getDeployment(appId, deploymentId) are the single calls underneath.

client.apps.listDeployments(appId, params?) and client.apps.rollback(appId, deploymentId, params?)

listDeployments returns the app's twenty most recent deployments, newest first. Each also carries current, whether its image is the one the app runs now, and rollbackable, whether the app can go back to it. Needs apps:read.

rollback puts the app back on the image an earlier deployment ran, without building, and returns the new deployment without waiting. It works for apps built from a repository as well as image apps. Only the image goes back: the app keeps its current environment variables and settings. Needs apps:write.

const { data: history } = await client.apps.listDeployments(appId);
const previous = history.find((d) => d.rollbackable);
if (previous) {
  const { data: rollback } = await client.apps.rollback(appId, previous.id);
  await client.apps.waitForDeployment(appId, rollback.id);
}

Hosting

Reads a project's shared hosting sites and where to reach their files. Needs an API key with hosting:read.

const { data: sites } = await client.hosting.list();
const { data: access } = await client.hosting.access(sites[0].id);
// access.username, access.host, access.sftp.port, access.ssh.{port,enabled}

client.hosting.get(siteId, { credentials: true }) also returns the site's sftpPassword. It needs no scope beyond hosting:read, so guard the key accordingly.

Site image, PHP version and email

A site runs one exact build of the site image and never changes build on its own, except for a fix to a critical or known-exploited vulnerability, which is applied during the night after forcedUpdate.deadline.

const { data: image } = await client.hosting.image(siteId);
// image.current.tag, image.update (null when up to date), image.forcedUpdate,
// image.rollback (with `until`), image.php.{version, securitySupportEnds, offered}

if (image.update) await client.hosting.updateImage(siteId);
await client.hosting.rollbackImage(siteId); // within 7 days of the last change
await client.hosting.setPhpVersion(siteId, { phpVersion: image.php.offered[0] });

updateImage, rollbackImage and setPhpVersion restart the site before they answer, and each returns the image status afterwards. Only the versions in php.offered are accepted; the API owns that list, so the SDK does not check it. They need hosting:write.

const { data: mail } = await client.hosting.mail(siteId);
// mail.enabled, mail.fromAddress, mail.domains — the verified email domains it can send from
await client.hosting.enableMail(siteId, { emailDomainId: mail.domains[0].id, localPart: "wordpress" });
await client.hosting.disableMail(siteId);

enableMail lets the site's PHP mail() send as <localPart>@<domain> through a new SMTP credential, and every email it sends is billed on the project's email plan. enableMail and disableMail need email:write as well as hosting:write.

const { data: domains } = await client.hosting.domains(siteId);
const { data: attached } = await client.hosting.attachDomain(siteId, { hostname: "shop.example.com" });
// attached.dnsRecordsCreated — true when Cosmoner runs the domain's DNS and added attached.instructions
await client.hosting.updateDomain(siteId, attached.domainId, { kind: "REDIRECT", redirectTarget: "https://example.com" });
await client.hosting.detachDomain(siteId, attached.domainId);

attachDomain takes one of the project's verified domains, or a subdomain of one; anything else is refused with a 422. A website is served from <hostname>/public_html unless path names another folder. updateDomain replaces the whole behaviour, so { kind: "SITE" } alone moves it back to its default folder. These need hosting:write; domains needs hosting:read.

Resource namespaces

Each of these works on one kind of resource and returns the API envelope. Every method takes { projectId } to override the client default, except projects, which reads across the account and never uses the default project. A method taking an id rejects an empty one before sending anything.

NamespaceReadChange
projectslist(), get(project) — by id or slugupdate(project, { name }), delete(project)
appsget(appId), logs(appId, { type: "BUILD" | "RUN" }), alongside list() and the deploy methodsupdate(appId, changes), delete(appId)
serverslist(), get(serverId) — adds the installed sshKeysupdate(serverId, { name }), powerOn(serverId), powerOff(serverId), reboot(serverId), delete(serverId)
sshKeyslist()create({ name, publicKey }), delete(sshKeyId)
databaseslist() (every kind), listDedicated(), getDedicated(databaseId), listShared(), getShared(tenantId)rotateSharedPassword(tenantId), deleteDedicated(databaseId), deleteShared(tenantId)
redislist(), get(redisId)delete(redisId)
domainslist(), get(domain) — by id or name, such as example.comcreate(name), verify(domain), delete(domain)
bucketslist() — there is no single-bucket readdelete(bucketId)
registrieslist(), get(registryId)delete(registryId)
iamlist(), get(iamUserName) — the list holds credentials plus partial-failure errorsdelete(iamUserName)
memberslist() — members and pending invitations
emaillistDomains(), getDomain(emailDomainId), limits(), alongside send()createDomain({ domainId }), createExternalDomain({ domainName }), verifyDomain(emailDomainId), deleteDomain(emailDomainId)

Two of these return a credential, and each needs only the namespace's read scope, so guard keys that carry it: databases.getDedicated always includes connectionUri, a full connection URI with the password, and redis.get always includes the plaintext password.

Ordering paid resources

servers.create, redis.create, databases.createDedicated, buckets.create, registries.create, hosting.create, apps.create and apps.resize charge the project's saved card at once, and are refused with a 402 before anything is created when the project cannot be billed. Each has a preview (previewDedicated, resizePreview) that returns the price first: monthly is exact, dueToday an estimate once the project has a subscription. client.catalog lists the sizes, plans and regions to order with, and needs no project. An app is ordered in two calls: createDraft saves what it should be, for free, and create turns the draft into a billed app.

Deletes are permanent, and a delete of a billed resource stops its billing. apps.update changes settings only, for free; it leaves environment variables to the deployment file. domains.create adds a domain you already own: publish the returned verificationRecord, then call verify. Deleting an SSH key leaves it on servers it was already installed on — stillAuthorisedOn counts them.

Power actions are asynchronous: the server reads PROVISIONING until the action settles, so read it again to see the outcome. powerOff cuts the power outright, and a stopped server is still billed. projects.delete is refused with 409 while the project still holds resources. email.createDomain sets up a domain already in the project, createExternalDomain one whose DNS is hosted elsewhere; publish the returned dnsRecords, then call verifyDomain, whose first success puts the project's email plan on its subscription. databases.rotateSharedPassword returns the new password this once, and the old one stops working at once.

Secrets

Stores the values a deployment file refers to with from_secret. Needs an API key with secrets:read, and secrets:write to change anything.

const { data: secret } = await client.secrets.create({
  name: "DB_PASSWORD",
  value: "hunter2",
  environment: "production",
});
secret.value; // "hunter2" — returned here and nowhere else
secret.maskedValue; // "hu••••r2", safe to display

const { data: secrets } = await client.secrets.list({ environment: "production" });
// name, environment, version, who changed it and when — never the value

A secret's value is encrypted at rest and returned exactly once, by the call that sets it. list() and get() describe a secret without its value, and no route decrypts one, so a lost value is replaced rather than recovered.

update(id, { value }) replaces a value and bumps version; delete(id) removes it; usage() reports how many secrets the project holds and may hold; audit(id) reads who changed it and when.

Two API behaviours are worth knowing before you debug them:

  • Writes need an owner or admin role. The API checks the role the key acts with independently of its scopes: a project key acts with its service account's role, a personal access token with its holder's. A key on a developer service account is refused with a 403 even when it carries secrets:write.
  • Creating is rate-limited to 100 per project every 10 minutes, shared by every key and machine working on the project, and a project at its secret limit answers 402.

Variables

The plaintext sibling of secrets, for non-sensitive configuration a deployment file refers to with from_variable. Needs variables:read, and variables:write to change anything.

await client.variables.create({ name: "LOG_LEVEL", value: "debug" });
const { data: variables } = await client.variables.list();
variables[0].value; // "debug" — returned in full on every read

That is the difference between the two resources, not an oversight: anything worth hiding belongs in client.secrets. update() here takes a value, a description, or both.

Deployment files

Validates a .cosmoner/deployment.yaml — the file you commit to describe how a repository deploys — without an API key or a network call.

import { validateDeployment } from "@cosmoner/sdk";
import { readFileSync } from "node:fs";

const { valid, issues, template } = validateDeployment(
  readFileSync(".cosmoner/deployment.yaml", "utf8")
);

for (const issue of issues) {
  console.log(`${issue.severity} ${issue.path}: ${issue.message}`);
}
error services.0.run_command: Static sites cannot define a run_command
warning services.0.prot: Unknown field "prot" — it will be ignored

Warnings are things the platform tolerates and you probably did not mean. An unknown key is the main one: the platform drops it rather than rejecting the file, so a template written for a newer field still applies its known settings against an older deploy. Treating that as fatal here would reject files the platform accepts, so it is reported as a warning that names the consequence. Pass strict when you would rather not let a typo through — it changes the verdict, not the finding.

template is the file as the platform reads it: defaults applied, unknown keys dropped. Comparing it against what you wrote is the point — it is the settings that will actually arrive.

The same check is available on the command line, for CI or a pre-commit hook, without installing anything:

npx @cosmoner/cli validate --strict

validateDeployment(source, options?)

ParameterType
sourcestringThe file as written — raw text, not a parsed object.
options.strictbooleanTreat warnings as errors. Defaults to false.

Returns { valid, issues, template }. validateDeploymentDocument(document, options?) is the same check over an already-parsed object, for a file that was generated rather than read from disk.

DEPLOYMENT_FILE_PATHS lists the locations the platform checks, in the order it checks them, and APP_SCHEMA_URL is the published JSON Schema an editor can be pointed at.

Error Handling

Every failure throws a subclass of CosmonerError, so you can catch broadly or narrowly:

import { Cosmoner, CosmonerError, RateLimitError } from "@cosmoner/sdk";

try {
  await client.email.send({ ... });
} catch (err) {
  if (err instanceof RateLimitError) {
    console.error(err.retryAfter); // seconds, when the API supplies it
  } else if (err instanceof CosmonerError) {
    console.error(err.code);    // e.g. "INSUFFICIENT_SCOPE"
    console.error(err.status);  // e.g. 403
    console.error(err.details); // field-level validation errors, when present
    console.error(err.docsUrl); // page explaining the fix, when the API links one
    console.error(err.message); // Human-readable message
  }
}
ErrorThrown on
ValidationError400, 422
AuthenticationError401
InsufficientScopeError403 — API key is missing a resource:action
NotFoundError404
ConflictError409
RateLimitError429
ServerError5xx
CosmonerTimeoutErrorRequest exceeded timeout
CosmonerConnectionErrorDNS, TCP, TLS or socket failure

版本与发布记录

@cosmoner/sdk 共发布 25 个版本,首次发布于 2026/09/16;过去 12 个月发布了 25 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.20.1latest2026/11/092 天前
1.20.02026/11/074 天前
1.19.12026/11/047 天前
1.19.02026/11/038 天前
1.18.12026/10/3011 天前
1.18.02026/10/2814 天前

关于 @cosmoner/sdk 的常见问题

@cosmoner/sdk 是什么?

Cosmoner SDK for Node.js

如何安装 @cosmoner/sdk?

在项目目录运行 npm install @cosmoner/sdk,或使用 pnpm add / yarn add / bun add。

@cosmoner/sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cosmoner/sdk 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@cosmoner/sdk 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@cosmoner/sdk 可以商用吗?

@cosmoner/sdk 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@cosmoner/sdk 的下载量有多少?

根据 npm 官方下载统计,@cosmoner/sdk 在 2026/10/11 – 2026/11/09 这 30 天内被下载 2,174 次,日均约 72 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cosmoner/sdk 还在维护吗?

最新版本 1.20.1 发布于 2026/11/09(2 天前);至今共发布 25 个版本,其中过去 12 个月发布了 25 个;首次发布于 2026/09/16。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。