Skip to content
Back to skills

Cupthread Swift Sdk

ASecurity

Guide for integrating and using the CupThread Swift SDK in iOS, macOS, visionOS, and tvOS applications.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 19, 2026
ai-agentsgoswiftgitapidocumentation

Works with

  • cursor
  • cli
  • api

Security analysis

A100/100

Scanned October 7, 2026

npx -y skills add CupThread/CupThreadAgenticCoding --skill cupthread-swift-sdk --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Cupthread Swift Sdk?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Cupthread Swift Sdk
[![Security: A β€” Skills Directory](https://www.skillsdirectory.com/api/skills/cupthread-cupthread-swift-sdk/badge)](https://www.skillsdirectory.com/skills/cupthread-cupthread-swift-sdk)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: cupthread-swift-sdk
description: Guide for integrating and using the CupThread Swift SDK in iOS, macOS, visionOS, and tvOS applications.
---

# CupThread Swift SDK Guide

The **CupThread Swift SDK** (`CupThreadFeedback`) provides native SwiftUI components and client APIs for integrating user feedback, feature requests, roadmaps, and changelogs into Apple platform apps (iOS 17+, macOS 14+, visionOS 1.0+, tvOS 17+).

## Documentation & Repository
- **Full API Documentation (DocC)**: [https://cupthread.github.io/CupThreadSwiftSDK/](https://cupthread.github.io/CupThreadSwiftSDK/)
- **GitHub Repository**: [https://github.com/CupThread/CupThreadSwiftSDK](https://github.com/CupThread/CupThreadSwiftSDK)

---

## πŸ€– Quick Prompt for Coding Agents

```text
Integrate the CupThread SwiftUI SDK (roadmap board, changelog overlay, and feedback composer) into this app. Scaffold a dedicated configuration helper with a placeholder for the App Key, and at the end, remind me with step-by-step instructions on how to set my App Key safely (e.g. via xcconfig or local config).
```

---

## Installation

### Swift Package Manager (SPM)
Add the package dependency in your `Package.swift` or via Xcode (**File > Add Package Dependencies...**):

```swift
dependencies: [
    .package(url: "https://github.com/CupThread/CupThreadSwiftSDK.git", from: "0.1.0")
]
```

### Prebuilt XCFramework Binary
Prebuilt XCFramework archives are distributed via GitHub Releases and the CupThread CDN:

```swift
targets: [
    .binaryTarget(
        name: "CupThreadFeedback",
        url: "https://cdn.cupthread.com/sdks/apple/CupThreadFeedback-0.1.0.xcframework.zip",
        checksum: "<sha256-checksum>"
    )
]
```

---

## Setup & Initialization

```swift
import SwiftUI
import CupThreadFeedback

// Initialize the feedback client with your App Key from CupThread Console
let client = FeedbackClient(
    configuration: FeedbackClientConfiguration(
        baseURL: URL(string: "https://api.cupthread.com")!,
        appKey: "app_xxx"
    )
)

// Obtain a persistent anonymous user token
let userToken = UserTokenStore.shared.token
```

---

## Ready-Made SwiftUI Views

Wrap your views with `CupThreadTheme(client:)` to automatically apply console appearance settings and theme styling.

- **`RoadmapBoardView(client:userToken:)`**: Kanban roadmap board grouped by public columns with vote counts and stage badges.
- **`FeatureRequestsView(client:userToken:)`**: Paged feature requests list with search, version filtering, optimistic voting, and submission flow.
- **`WhatsNewView(client:userToken:)`**: Interactive release notes / changelog with Markdown formatting and email subscription.
- **`ChangelogOverlayView` / `.changelogOverlay(client:isPresented:)`**: In-app modal sheet announcing new release notes.
- **`FeedbackComposerView(client:userToken:onSubmit:)`**: Structured feedback form supporting text details, device metadata, and image/log attachment uploads.

### Example Usage

```swift
struct MyFeedbackView: View {
    let client: FeedbackClient
    let userToken: String
    @State private var showWhatsNew = false

    var body: some View {
        CupThreadTheme(client: client) {
            NavigationStack {
                RoadmapBoardView(client: client, userToken: userToken)
            }
            .changelogOverlay(client: client, isPresented: $showWhatsNew)
        }
    }
}
```

---

## Payment-Attribute Signing (HMAC-SHA256, DATA-03)

**SDK status β€” the Swift SDK signs automatically.** Configure `FeedbackClientConfiguration(signingSecret:)` (or pass `signingSecret` on an individual `updateUserAttributes` call) and the SDK attaches `signature` + `timestamp` itself whenever payment attributes are present and a secret resolves. Hand-rolling is only needed for custom transports; the public `UserAttributesSigner` API gives you the canonical string and HMAC if you must sign manually.

`PUT /api/v1/public/apps/{appKey}/user` only persists paying status, plan, MRR, and currency when the request is **signed with the app's SDK signing secret** (developer console: *App Access β†’ App Credentials β†’ SDK signing secret*). Whenever the body contains any of `isPaying`, `mrr`, `plan`, or `currency` (an explicit JSON `null` counts), it must also carry `signature` (64-char hex HMAC-SHA256, case-insensitive) and `timestamp` (epoch seconds) β€” both plain body fields. Since DATA-07, `currency` is a signed attribute too (it is the unit of `mrr`), so identity-only writes are the only unsigned path left; a signed update that omits `currency` preserves the stored currency instead of coercing it to `USD` β€” `USD` applies only when a profile is created with no stored currency. Rejections happen before any profile row is created:

- `422 payment_attributes_require_signature` β€” payment fields without `signature` + `timestamp`
- `422 sdk_signing_secret_not_configured` β€” the app has no signing secret yet
- `401 stale_signature` β€” `timestamp` more than Β±300 s from server time
- `401 invalid_signature` β€” wrong key or tampered values

**Canonical string** β€” HMAC-SHA256-sign the exact bytes of this newline-joined string (no trailing newline) and hex-encode the digest:

```
cpt-user-attrs-v1
<appKey>
<userToken>
<isPaying: true|false|unset>
<plan: value|null|unset>
<mrr: canonicalNumber|null|unset>
<currency: valueAsSent|unset>
<timestamp: epochSeconds>
```

- Absent fields sign as `unset`, explicit JSON `null` as `null` (omitting `plan` β‰  sending `"plan": null`). `userToken` is the body value when present, else the `X-User-Token` header value.
- `canonicalNumber` follows JS `Number.prototype.toFixed(2)` with trailing zeros and a trailing `.` stripped (`1200.00 β†’ "1200"`, `99.50 β†’ "99.5"`); exact binary ties pick the larger n (`10.125 β†’ "10.13"`). Compute it from the raw JSON value as sent.
- Sign immediately before sending (Β±300 s freshness window) and don't mutate signed values when appending `signature`/`timestamp` to the body.

```swift
import CryptoKit

func userAttrsSignature(canonical: String, secret: String) -> String {
    let mac = HMAC<SHA256>.authenticationCode(
        Data(canonical.utf8),
        using: SymmetricKey(data: Data(secret.utf8)))
    return mac.map { String(format: "%02x", $0) }.joined()
}
```

On targets without CryptoKit, CommonCrypto's `CCHmac(kCCHmacAlgSHA256, key, keyLength, data, dataLength, out)` yields the same digest.

**Cross-check your port** against this reference vector before shipping: secret `cpt_sk_test_secret_0123456789abcdef`, appKey `app_demo12345`, userToken `3fa85f64-5717-4562-b3fc-2c963f66afa6`, timestamp `1758000000`, body `{"isPaying":true,"plan":"pro","mrr":299.5,"currency":"USD"}` β†’ canonical `cpt-user-attrs-v1\napp_demo12345\n3fa85f64-5717-4562-b3fc-2c963f66afa6\ntrue\npro\n299.5\nUSD\n1758000000` β†’ signature `59bc1177751f4c36f9caeed8c763cde9ee18835196acd1b6d4ce3b1ea2dc0273`. `cupthread api sign-user-attrs` generates more vectors.

## Key Features Overview

1. **Feedback & Attachment Uploads**: Submit categorized feedback with automatic device info and screenshot/log attachments.
2. **Feature Request Voting**: Real-time optimistic upvoting, search, and submission for user-driven features.
3. **Roadmap & Kanban**: Visualize planned, in-progress, and completed roadmap milestones.
4. **Changelog & "What's New"**: Display rich Markdown release notes with email update subscription.
5. **User Attributes Sync**: Synchronize paying status, plan, MRR, and currency via `client.updateUserAttributes(...)`. The underlying `PUT /api/v1/public/apps/{appKey}/user` endpoint is rate limited per client IP (60 requests/minute, HTTP `429`) β€” syncs of many users behind one shared IP must retry with exponential backoff.

## Request Correlation IDs (X-Request-Id)

Every CupThread API response carries an `X-Request-Id` header (OPS-01): your ID when the request supplied a format-valid one (`^[A-Za-z0-9._-]{8,64}$`, e.g. a UUID), otherwise a server-generated UUID. Quote it verbatim in bug reports and support requests β€” it lets CupThread locate the exact request in server logs. When driving the API with a raw `URLSession` call instead of the SDK, send your own `X-Request-Id` per request; values outside that charset/length budget are silently ignored and replaced. CORS exposes the header, so browser-based integrations can read it too.

## Feedback Image Restrictions (415 Unsupported Media Type)

The feedback image upload endpoint accepts only **PNG, JPEG, WebP, and GIF**. When integrating the attachment picker:

- **Drop SVG from image pickers / file-type allowlists.** `image/svg+xml` is rejected with `415` β€” declared via MIME type, or via a `.svg` filename when no content type is sent. SVG executes script in browsers and is never stored from end-user uploads.
- **The declared MIME type must match the file content.** The server sniffs magic bytes; mismatched or unrecognized files (e.g. HTML bytes named `.png`) fail with `415 {"error": "File content does not match declared image type (...)"}`.
- **Map HTTP `415` to a user-facing "unsupported image type" message** and let the user pick a different file. It is a deterministic client error β€” never retry automatically.
- **Image uploads are private attachments (PRIV-02).** `POST /api/v1/uploads/images` answers `200` `{kind: "image", uploadId, status: "uploaded", stored, filename, mimeType, size, sha256}` β€” no `url`, `key`, or `variants`. Do not fetch the file from a public CDN URL or `GET /api/v1/files/:key` (`403 private_attachment_forbidden`). After `POST /api/v1/feedback` finalizes the upload, an authorized caller downloads it from `/api/v1/feedback/attachments/:id/download`.

Console-configured app icons (developer-facing) are unaffected and may still use screened SVG.

## Self-Service Data Erasure (PRIV-01)

`POST /api/v1/me/erase` lets end users erase their own profile for one app (PRIV-01). Call it with a `{"appKey": "…"}` JSON body and the user's `X-User-Token: <uuid>` header (a Clerk session also works for signed-in users; developer `cpt_` tokens are not an identity here):

- **Effect**: the anonymous token is rotated immediately (the old token stops working; replays return `404`), stored PII (display name, email, IP, user agent) is cleared, and the user's feature requests, votes, and comments survive **without personal attribution** β€” aggregate counts are preserved.
- `200 {"erased": true, "endUserId": "…"}` on success β€” discard the stored `X-User-Token` locally and mint a fresh one for any further activity.
- `400` `{"error": "appKey is required"}` without `appKey`; `401` without any identity; `404 {"erased": false, "error": "…"}` when the app is unknown, no profile matches, or the profile was already erased; `429` when rate limited (first a per-IP budget of 10 requests/60 s answering `Too many submissions…`, then a per-token budget answering `Too many erasure requests for this token…`).
- Erasure is immediate and **irreversible** β€” confirm with the user in UI (e.g. a settings/"delete my data" action) before calling. Never retry `404`; retry `429` with exponential backoff.

## Feedback Metadata Redaction (PRIV-01)

The `metadata` map attached to feedback submissions is **sanitized server-side before storage** β€” oversized or credential-looking payloads are shrunk, **never rejected**, so existing integrations keep working unchanged. Pre-sanitizing client-side with the same rules is encouraged so users get local feedback:

- Keys must match `[A-Za-z0-9_.:-]{1,64}` (max **24** keys); non-conforming or surplus keys are silently dropped.
- Values under credential-looking keys β€” `token`, `secret`, `password`, `apiKey`, `authorization`, `cookie`, `session`, … (camelCase-aware, so `github_token`, `api-key`, and `sessionCookie` all hit) β€” are replaced with the literal `"[redacted]"`.
- Strings are truncated to 512 chars (suffixed `…[truncated]`), nesting is capped at depth 4 (deeper objects/arrays become `null`), and the serialized object is capped at **8 KB** (whole keys dropped in sorted order until it fits β€” never sliced mid-value).

## Feedback Attachment Upload Flow (Upload Sessions)

Attachment uploads go through **pre-allocated upload sessions** β€” feedback submissions referencing raw attachment URLs or storage keys are rejected (`400` `direct_attachment_forbidden`). For custom integrations outside the SDK composer, drive the three-step lifecycle directly:

1. **Create a session first**: `POST /api/v1/uploads/sessions` with `{ "appKey": "…", "files": [{ "clientFileId": "…", "filename": "…", "mimeType": "…", "size": 123 }] }` (1–8 files). Anonymous users must send the **same `X-User-Token`** they will use for feedback submit β€” the session is identity-bound (SEC-28). The `201` response provides `sessionToken` (`cpt_up_…`, ~1-hour TTL), `expiresAt`, and `uploads[]` slots with `uploadId` (`upl_…`), `uploadUrl`, and `maxBytes`.
2. **Upload each reserved slot**: `PUT /api/v1/uploads/{uploadId}` with `Authorization: Bearer <sessionToken>` and the raw file bytes (`Content-Type` set to the file's real MIME type; `multipart/form-data` with a `file` field also works). Slots are single-shot (`409` `already_uploaded`), over-cap bodies fail with `413` `file_too_large`, and failed content inspection fails with `415` β€” deterministic, never retry.
3. **Submit feedback with the ids**: pass the finalized `uploadId`s (max 8) in the `uploadIds` array of `POST /api/v1/feedback`. A scan-rejected attachment fails the whole submission with `422` `scan_rejected` β€” remove or replace that file and resubmit; the passing uploads stay reusable. Two concurrent submits of the same `uploadId` no longer race into a `500`: the loser gets `409` `already_finalized` and creates nothing β€” do not resubmit that id.

Use the per-file `maxBytes` from the session response to pre-validate file sizes client-side, and reuse one session for all files of a single composer submission.

## Feature-Request List Pagination (Cursor Keyset)

The public feature-request feed β€” `GET /api/v1/feature-requests` β€” is keyset-cursor-paginated (DATA-01). Every response carries `requests`, `total`, `hasMore`, and `nextCursor`; walk large boards by echoing `nextCursor` back as the `cursor` query parameter until it returns `null`:

- **Prefer `cursor` over incrementing `offset`** β€” offset pages scan and discard rows server-side, while the cursor jumps straight to the next key. A request that sends `cursor` ignores `offset`.
- **`offset` above 10000 is clamped to 10000** and still returns `200`. Deep pages belong on the cursor. Plain listings (no `q`) are rate limited per client IP at 60 requests/minute (generic `429` JSON).
- **Treat the cursor as opaque** β€” never parse, construct, or persist one beyond forwarding it back; malformed cursors fail with `400 {"error": "Invalid cursor"}`.
- **`hasMore` is exact** (the server fetches one extra row) and `total` stays constant across pages; `limit` is clamped to 1–200 (default 50), and `q`/`versionId` filters compose with the cursor.
- **The built-in roadmap/feedback screens need no changes** β€” the fields are additive; clients that only read `requests` keep working.

## Feature Request Vote Rate Limits (429 Too Many Requests)

The public vote endpoints β€” `POST` / `DELETE /api/v1/feature-requests/{id}/vote` β€” are rate limited **per client IP** to **20 requests per minute**. Throttled calls fail with `429 {"error": "Too many votes. Please try again shortly."}` (a vote-specific body, distinct from the generic `Too many requests…` text). When building custom voting UI on top of the client:

- **Optional `shipNotifyEmail`** (an email string, max 254) on `POST` enrolls that address in double-opt-in ship notifications ("email me when this ships"). Omitting it leaves the `{voted, voteCount}` response unchanged. On a sign-in-required board, an address that is not one of the session's verified emails still records the vote and adds `warning.code` `email_not_verified` with `Ship notifications on this board are bound to your signed-in email address`; no confirmation email is sent. Boards that allow anonymous voting accept any address.
- **Treat `429` as a recoverable, user-facing condition** β€” surface a friendly "you're voting too fast, try again in a minute" message rather than a generic error.
- **Never auto-retry `429` in a tight loop** β€” if you retry at all, back off for the remainder of the 60-second rate-limit window.
- **The built-in roadmap/voting screens need no changes** β€” normal usage (voting on a handful of feature requests) stays well under the limit.

## Public Board Read Limits and Shared Cache

These four public GETs share one per-IP budget of **60 requests per minute** and answer `429 {"error": "Too many requests. Please try again shortly."}` when it is spent:

- `GET /api/v1/feature-requests/{id}/comments`
- `GET /api/v1/public/apps/{appKey}/changelog`
- `GET /api/v1/public/columns/{appKey}`
- `GET /api/v1/public/versions/{appKey}`

A board or changelog screen that loads once stays under that budget. A poller, or a widget that refetches on a tight timer from one shared IP, will not.

- **Retry `429` after a short backoff.** The window is one minute. Treat it as transient. An SDK that already surfaces non-2xx needs no new status mapping.
- **Anonymous `200`s can be up to 30 seconds stale.** The server may serve them from a shared cache, and a cache hit carries `Cache-Control: public, max-age=30`. A comment just posted, a column just edited, a version just released, or a changelog just published can take that long to appear on an anonymous read. Polling faster spends the same 60/minute budget and does not expire the cache.
- **The changelog feed splits by session.** Only anonymous responses are cached. A Clerk session gets a fresh body, and linked feature-request titles stay visible only to callers who can see the roadmap. Columns, versions, and comment threads are the same for every caller, so those `200`s are shared.
- **A sign-in-required board still returns `401`** while an older `200` sits in the cache. The built-in roadmap and changelog screens need no code change for this.

## Public Profile Rate Limits & Unknown-User 404 (SEC-34)

The public profile page / hovercard reads `GET /api/v1/users/{userId}/profile`, which is rate limited **per client IP** to **60 requests per minute** β€” one bucket shared with the `PUT /api/v1/public/apps/{appKey}/user` attribute sync. Throttled calls fail with the generic `429 {"error": "Too many requests. Please try again shortly."}`. An unknown app-scoped `u_*` id (or a `u_*` id sent without `appKey`) returns `404 {"error": "User profile not found"}` instead of a placeholder:

- **Cache profile lookups client-side** and reuse them across renders. A card list resolving many users behind one shared IP (office NAT, CI farm) can exhaust the 60/minute budget β€” which also throttles attribute syncs from the same IP.
- **Handle `429` with exponential backoff** and render a friendly "try again shortly" state; never retry in a tight loop.
- **Treat `404` on a `u_*` id as "no public profile for this id"** and fall back to the placeholder UI β€” the server no longer scans for unmatched ids, so retrying cannot change the answer. Raw `user_*` ids from old `/u/` links keep the opt-in placeholder behavior.

## Request Body Limits & End-User Description Images (SEC-36 / PRIV-11)

Two server-side behaviors every integration should know:

- **Public JSON budget is 256 KB (SEC-36).** Public intake routes (feature requests, votes, comments, user-attribute upserts, feedback) parse the body through a bounded reader **before** rate limiting and validation; anything over the budget fails with `413 {"error": "Payload exceeds size limit", "code": "payload_too_large"}` without consuming a rate-limit slot. Keep request bodies β€” especially free-text description fields β€” well under 256 KB, truncating oversized input client-side instead of discovering the limit via `413`.
- **Image embeds in end-user feature-request descriptions do not render (PRIV-11).** Public boards strip image embeds (`![alt](url)`, reference images, raw `<img>`) from end-user-submitted descriptions at ingest and render descriptions with images disabled β€” anti tracking-pixel/phishing hardening. Links still render (tagged `rel="nofollow ugc noopener noreferrer"`); submitting an image embed is not an error, it just will not display. Developer-authored content (changelog entries, console notifications) keeps full Markdown rendering.

For complete method signatures, customization options, and advanced architecture, consult the [DocC API Documentation](https://cupthread.github.io/CupThreadSwiftSDK/).

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.