
Security News
Lovable’s OJ Rewrites Vite’s Dev Server in Rust as AI Lowers the Cost of Forking Open Source
Lovable’s OJ rewrites Vite’s dev server in Rust, reducing memory use and preview times as AI lowers the cost of open source reimplementation.
@simplepush/sdk
Advanced tools
TypeScript SDK for Simplepush.
ESM-only. Targets Node 20+, Bun, Deno, and modern browsers (browser usage requires a custom webSocketFactory since the standard WebSocket API can't send headers).
bun add @simplepush/sdk
# or
npm install @simplepush/sdk
import { Client } from "@simplepush/sdk";
const client = new Client({
apiToken: process.env.SP_API_TOKEN!, // required
passwords: [["hunter2", "my-topic"]], // [password, topic] pairs; or "account-pw" for the default
});
for await (const event of client.events({ since: new Date(Date.now() - 86_400_000).toISOString() })) {
console.log(event);
}
Two parallel aggregates: sendTask (multiple inputs, replies) and
sendNotification (a single text/choice/actions input, no replies). Both take a
single options object; the send target (topic for a personal Client, or
topic / member / broadcast on an OrgClient) lives in that object. Omit
the target on a personal Client to send to your own devices (a self-send,
encrypted under the Personal Password: the per-send password, else the
configured one).
Ids are type-prefixed strings.
taskId,subtaskId, input/reply/file ids and the like come back type-tagged —tsk_…,sub_…,inp_…,rfl_…— not bare UUIDs.
import { Client } from "@simplepush/sdk";
const client = new Client({ apiToken: process.env.SP_API_TOKEN! });
// A task send returns a TaskGroup: every recipient gets their OWN independent
// task instance (one recipient's answers never touch another's task).
const group = await client.sendTask({
topic: "deploys",
title: "Deploy v1.2.3?",
inputs: [{ type: "choice", options: ["yes", "no"], required: true }],
});
const task = group.sole; // single-recipient topic; iterate group.instances for many
for await (const ev of task.inputs()) {
if (ev.kind === "taskCompleted") console.log(ev.uploads);
}
// A notification: lighter — a single input, one completion event. A personal
// topic send defaults to independent mode, so this returns a NotificationGroup
// (pass `shared: true` to get one shared Notification instead).
const notifGroup = await client.sendNotification({
topic: "deploys",
title: "Build failed",
content: "main @ a1b2c3 failed 3 tests",
input: { type: "choice", options: ["ack", "mute"] },
});
for await (const ev of notifGroup.inputs()) {
console.log(ev.item.reply); // NotificationTextReply | NotificationChoiceReply | NotificationActionReply | undefined
}
Task inputs: text, choice (set multi: true, with optional
minSelections/maxSelections), actions (styled buttons; the tapped
action's stable key comes back), slider (min/max/step/unit),
photo, voiceRecording, file, location. A notification input is text,
choice, or actions; a notification can also carry ONE media item — image
(iOS + Android) or audio (iOS only), an uploaded FileAttachment or a URL
string — and critical: true (iOS Critical Alert).
// Richer inputs: action buttons, a slider, multi-choice, a photo request.
const incident = await client.sendTask({
topic: "ops",
title: "Incident 4711",
inputs: [
{ type: "actions", required: true, actions: [
{ key: "ack", label: "Acknowledge", style: "primary" },
{ key: "escalate", label: "Escalate", style: "destructive" },
]},
{ type: "slider", min: 0, max: 10, step: 1, unit: "sev", required: true },
{ type: "choice", options: ["db", "api", "infra"], multi: true, required: false },
{ type: "photo", required: false },
],
});
for await (const ev of incident.inputs()) {
if (ev.item.kind !== "taskCompleted") continue;
for (const u of ev.item.uploads) {
if (u.kind === "action") console.log(ev.recipient?.name, "pressed", u.key);
if (u.kind === "slider") console.log("severity", u.value);
if (u.kind === "multiChoice") console.log("areas", u.values);
if (u.kind === "photo") await u.save("./incident-4711");
}
}
By default each filled input arrives as an intermediate input event, then the
terminal taskCompleted carries the full committed set; autoCommit: false
has the recipient submit the whole form at once instead. Other send options:
reply: "one-shot" | "sticky" | "one-time-per-user" opts recipients into the
in-thread reply composer (collect via replies()), and
contentFormat: "markdown" renders content as Markdown.
Attachments. files uploads local files alongside a task/subtask
(encrypted when the send is); the SDK is environment-agnostic, so you pass the
bytes. A notification takes its single media item the same way, or as a URL:
import { readFile } from "node:fs/promises";
await client.sendTask({
topic: "reports",
title: "Q3 numbers",
content: "Full report attached.",
files: [{ filename: "q3.pdf", data: await readFile("q3.pdf"), contentType: "application/pdf" }],
});
await client.sendNotification({ topic: "alerts", title: "Door cam", image: "https://cam.example/last.jpg" });
Streams accept { replay, idleMs, signal }: replay emits the buffered backlog
since the send, idleMs ends the stream after that many ms of silence, and
signal cancels it.
File downloads. The binary upload objects (photo/voice/file uploads and a
reply's photo/file/audio) are download handles bound to the client that yielded
them: read() returns the bytes (checksum-verified, decrypted on encrypted
chains), save(path) writes to disk (a directory uses the file's own name),
and downloadUrl() presigns the raw short-lived (~5 min) S3 URL. Downloads
authenticate with the client's apiToken/apiKey and are unlimited. Failures
throw DownloadError.
for await (const ev of task.inputs()) {
if (ev.kind === "taskCompleted") {
for (const u of ev.uploads) {
if (u.kind === "photo") await u.save("./captures");
}
}
}
for await (const r of task.replies()) {
if (r.kind === "reply" && r.photo) console.log(await r.photo.read());
}
Encryption. Pass password per send to encrypt the
body; the returned handle decrypts the recipient's replies. Or configure the
client's passwords with [password, topic] pairs — sends to those topics
encrypt automatically (a per-send password overrides):
const client = new Client({ apiToken, passwords: [["hunter2", "deploys"]] });
await client.sendTask({ topic: "deploys", content: "🤫" }); // encrypted via the "deploys" pair
await client.sendTask({ topic: "other", content: "!", password: "x" }); // per-send password
A task can have subtasks appended to its chain. A subtask inherits the
parent's recipients and encryption (no target, no password); its inputs() /
replies() are scoped to it, and stream off the same shared connection.
const sub = await task.append({
title: "One more thing",
inputs: [{ type: "text", required: true }],
});
for await (const ev of sub.inputs()) {
if (ev.kind === "subtaskCompleted") console.log(ev.uploads);
}
By default every recipient of a task send gets their own independent instance,
grouped under a TaskGroup:
const group = await client.sendTask({ topic: "deploys", content: "check in" });
group.groupId; // grptsk_… — the group handle
for (const task of group.instances) {
// a full per-recipient Task: its own taskId, appendToken, streams
console.log(task.taskId, task.recipient?.publicId, task.recipient?.name);
}
group.append(...) appends a subtask to every member's chain atomically and
returns one Subtask per member; pass instances: [taskId, …] to reach only
some members. File attachments are uploaded ONCE for the whole batch — every
sibling subtask references the same attachment.
const subs = await group.append({ content: "follow-up" }); // all members
await group.append({ content: "just you", instances: [group.instances[0]!.taskId] }); // a subset
Pass shared: true to sendTask for shared mode — ONE task all
recipients see and answer together, returned as a plain Task.
activity() (on Task and TaskGroup) merges input events AND replies over
one subscription. To collect from a FRESH process, rebuild an observe-only
handle from persisted ids: client.watchTaskGroup({ groupId, createdAt, members }) / client.watchNotificationGroup(...) — same streams, but no
tokens and no append(); pass createdAt to backfill everything since the
send.
Per-call passwords. The receive side takes passwords per call, in the
same shape as the constructor's: [password, topic] pairs and/or one bare
Personal Password string. The keys stay with that call. watchTaskGroup,
watchSubtask, watchSubtaskGroup, watchNotificationGroup, submissions,
downloadFile and the id-addressed cancelTask / cancelSubtask /
cancelTaskGroup (which seal the note) all accept it:
const group = client.watchTaskGroup({ groupId, members, passwords: [["hunter2", "deploys"]] });
await client.cancelTask(taskId, { note: "superseded", passwords: [["hunter2", "deploys"]] });
const file = await client.downloadFile(taskId, fileId, { passwords: "personal-pw" });
A submission is self-authored user content — a text body plus an optional
photo and file — pushed into a user's own stream with no
associated task; a task reply without the task. Submissions are created by the
app; the SDK observes them on the client's feed (both Client and
OrgClient):
for await (const sub of client.submissions({ idleMs: 300_000 })) {
// sub: Submission — body?: TextBody, photo?/file?: SubmissionFile
if (sub.photo) await sub.photo.save("./inbox");
}
photo/file/audio are download handles (read() / save() / downloadUrl()). Audio attachments carry durationSeconds (voice-recording length in seconds). Location data (latitude, longitude, accuracy, altitude, heading, speed, timestamp) is provided inline via location when available; when the parent is encrypted it is decrypted with the same key as the body.
since (ISO 8601) resumes from that point, backfilling earlier submissions;
idleMs ends the stream after that many ms of silence; signal cancels it.
Encrypted submissions are decrypted with your account default password (not
a topic password). Pass it in passwords (a bare string):
const client = new Client({ apiToken: TOKEN, passwords: "your-default-password" });
Or pass it per call (personal Client only): client.submissions({ password })
overrides the account password for that call.
OrgClient authenticates with the org apiKey and addresses sends with
exactly one target: { topic }, { member } (by member name), or
{ broadcast: true }. Encryption is automatic: pass the org's master key(s)
(from your org's encryption vault; the SDK can't derive them) and every send is
encrypted under the current key — there are no per-send passwords. Without
keys, sends go out in the clear and org ciphertext is left as-is.
import { OrgClient } from "@simplepush/sdk";
const org = new OrgClient({
apiKey: process.env.SP_API_KEY!,
orgMasterKey: masterKeyBytes, // Uint8Array(32)
orgMasterKeyVersion: 3, // or several: orgMasterKeys: [{ version, key }]
});
const group = await org.sendTask({
broadcast: true,
title: "All hands?",
inputs: [{ type: "choice", options: ["yes", "no"], required: true }],
});
for await (const ev of group.inputs()) {
console.log(ev.recipient?.name, ev.item.kind); // recipient = the org member
}
Everything else works as on a personal Client: independent-mode groups (the
member handle/name rides on each instance's recipient), subtasks, streams,
submissions, downloads.
FAQs
TypeScript SDK for Simplepush.
The npm package @simplepush/sdk receives a total of 194 weekly downloads. As such, @simplepush/sdk popularity was classified as not popular.
We found that @simplepush/sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Lovable’s OJ rewrites Vite’s dev server in Rust, reducing memory use and preview times as AI lowers the cost of open source reimplementation.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.