@fytkit/sdk

中文文档
5.1.2发布于 2026/11/12UNLICENSEDTypeScriptES Module近 30 天下载 2180

用于 fytkit 公共 API 的 Vanilla TypeScript SDK

中文使用文档
文档目录14 节
  1. 安装
  2. 30秒快速启动
  3. 加入一个工作室并启动
  4. 训练员预览
  5. 追踪健身
  6. 二因子登录
  7. 2FA 注册和账户安全
  8. 拒绝
  9. 实时聊天
  10. 通知偏好
  11. 推动通知
  12. 此预览中包含的内容
  13. 储存权衡
  14. 下一个

@ fytkit/ sdk (英语)

Vanilla TypeScript SDK用于fytkit公共API. 在您的 fytkit 程序、 工作、 食谱和 Mux 保护的视频上, 建一个定制的见习软件 .

v0.1.x是一个前1.0放行. 每种导出类型的形状在 etc/sdk.api.md; 断开更改记录在 CHANGELOG.md。 。 。 .

安装

pnpm add @fytkit/sdk @orpc/client
# or
npm install @fytkit/sdk @orpc/client

在SDK为前 1. 0 时按精确的版本 :

pnpm add @fytkit/[email protected] @orpc/client

完全安装 + 版本连接 : docs/developer/installation.md. semver政策: docs/developer/concepts/versioning.md。 。 。 .

30秒快速启动

import { createFytkitClient } from "@fytkit/sdk";

const fytkit = createFytkitClient({
  clientId: "fk_app_xxxxxxxxxxxxxxxxx", // from /dashboard/developer
});

// Sign a trainee in (your own auth UI calls this)
const session = await fytkit.auth.signIn({
  email: "[email protected]",
  password: "their-password",
});

// Fetch their workouts
const { items } = await fytkit.workouts.list({ limit: 20 });

// Subscribe to auth state (e.g. to redirect on sign-out)
const off = fytkit.auth.onAuthStateChange((state) => {
  if (state.type === "signed_out") window.location.assign("/sign-in");
});

// Sign out — revokes the server-side session and clears local storage
await fytkit.auth.signOut();
off();

加入一个工作室并启动

一个Fytkit教练风格的应用从工作室的联想代码(它的QR代码或 并阅读工作室的发射设置:

import { createFytkitClient, resolveJoinCode, resolvePaymentMode } from "@fytkit/sdk";

// Once, when the member scans or types the code: keep the client id.
const studio = await resolveJoinCode(scannedCode);
const fytkit = createFytkitClient({ clientId: studio.clientId });

// Every launch: revalidate the stored settings with their ETag.
const stored = await loadStoredBootstrap(); // { data, etag } | null, your storage
const result = await fytkit.app.bootstrap({ etag: stored?.etag });
if (result.status === "ok") await saveStoredBootstrap({ data: result.data, etag: result.etag });
const settings = result.status === "ok" ? result.data : stored?.data;
// settings.studio (name, logoUrl), settings.theme (palette, variants),
// settings.featureOverrides, settings.minAppVersion, settings.links,
// settings.realtime, settings.paymentPolicy

// "Subscribe" (a link out to the studio's checkout) only where the store allows it.
const { mode } = resolvePaymentMode(settings.paymentPolicy, {
  platform: "ios",
  storefront: storefrontCountryCode, // StoreKit's Storefront.current?.countryCode
});

app.bootstrap 决定: { status: "unchanged" } (a 304,无尸体) 页:1 etag 您发送是当前。 换了 appConfig.get 在4.0.0中选择: app.bootstrap,与 逐地迁移. resolveJoinCode 不需要客户端. resolvePaymentMode 是计划信息 ("你的计划包括什么") 保险单上没有列出每个商店: 订阅或您的计划包括。 。 。 .

训练员预览

教练不能签入会员应用程序:员工账户在那里被拒 (单位:千美元)first_party_account)取而代之的是App →主题上的"app中的审查" 仪表板显示链接的QR代码,例如: https://www.trainerlane.com/app-link/preview?client=<clientId>&token=<token>. 培训员Lane 教练赎回来的 没有成员的工作室只读预览 (未释放;下一个未成年人的船舶):

import { createFytkitClient, createMemoryStorageAdapter, refusalData } from "@fytkit/sdk";

// Start from the link's `client` and `token`. Use a dedicated client: the
// phone may be bound to no studio, or to another one, and a preview never
// shares a client with a signed-in member.
const url = new URL(scannedLink);
const preview = createFytkitClient({
  clientId: url.searchParams.get("client") ?? "",
  storage: createMemoryStorageAdapter(),
});
const session = await preview.auth.startPreview({ token: url.searchParams.get("token") ?? "" });
// session: { kind: "preview", accessToken, expiresAt }: show a Preview banner.

// Studio content works as for a member on any plan: app.bootstrap, programs,
// workouts, exercises, articles, recipes, nutrition, media, plans.
const programs = await preview.programs.list({});

// Anything a member owns (profile, progress, logs, check-ins, habits,
// messages, health) and every write is refused with `preview_mode`:
// show the screen's empty state.
try {
  await preview.progress.getStreak();
} catch (err) {
  if (!refusalData(err, "preview_mode")) throw err;
  showEmptyState();
}

// End it (signOut does the same while previewing).
await preview.auth.endPreview();
  • 一次,15分钟内. QR令牌在15分钟之内赎回一次 显示。 一个未知的,过期的,使用过的 或其他工作室的代号拒绝 preview_token_invalid (单位:千美元)not_found:向教练索取新代码. A 类 工作室自己的应用程序(不是Fytkit教练) preview_unavailable。 。 。 .
  • 60分钟,只有记忆. 预览会持续60分钟,永远不会 清新而永无止境 storage,所以重装结束。 结束于 expiresAt打开 endPreview 或者说 signOut,任何401(工作室转向) 当一个成员签到同一个客户时 每端 但最后的喷出 signed_out· ; onPreviewChange 报告每个开始和 结束.
  • 不与一员并. startPreview 本地拒绝 member_session_active (单位:千美元)conflict),而一名成员则在其中签字 客户端.

反应 : usePreviewSession, (中文(简体) ), useStartPreview 财务报告和审定财务报表 useEndPreview 内 @fytkit/react (释出. 每一种方法: auth.startPreview。 。 。 .

追踪健身

登录一个解决临时选择( 不需要程序注册), 记录集, 完成 然后读回进展:

// Start an ad-hoc log against any workout — or pass { sessionId, enrollmentId }
// instead to log a scheduled program session.
const { data: log } = await fytkit.sessionLogs.create({ workoutId });

await fytkit.sessionLogs.addSet({
  id: log.id,
  exerciseId,
  setNumber: 1,
  reps: 8,
  weightKg: "80.00",
});

await fytkit.sessionLogs.update({ id: log.id, completedAt: new Date().toISOString() });

// Workout-by-workout history for that workout
const { data: history } = await fytkit.sessionLogs.list({ workoutId });

// Stored progress: streak, PR history, aggregate summary
const { data: streak } = await fytkit.progress.getStreak({});
const { data: prs } = await fytkit.progression.listPrHistory({ exerciseId });
const { data: summary } = await fytkit.progress.getSummary({ period: "12w" });

二因子登录

auth.signIn 返回歧视性结果。 当学员有2FA时 启用时,以他们选择的方法完成挑战:

const result = await fytkit.auth.signIn({ email, password });

if (result.kind === "two_factor_required") {
  const { challenge } = result; // challenge.availableMethods: "totp" | "backup_code" | "email_otp"

  // TOTP from their authenticator app…
  const session = await fytkit.auth.verifyTotp({ challenge, code: "123456" });

  // …or a one-time backup code:
  // await fytkit.auth.verifyBackupCode({ challenge, code });

  // …or an emailed code:
  // await fytkit.auth.requestEmailOtp({ challenge });
  // await fytkit.auth.verifyEmailOtp({ challenge, code });
}

每个验证方法缓存已发行的会话完全像普通的登录 (单位:千美元)signed_in 开火 onAuthStateChange) (中文(简体) ).

2FA 注册和账户安全

所有这一切都需要签署会议:

// Enroll: render totpUri as a QR code; show backupCodes exactly once.
const { totpUri, backupCodes } = await fytkit.auth.enableTotp({ password });
await fytkit.auth.verifyTotpSetup({ code: "123456" }); // SDK swaps the rotated token for you

const status = await fytkit.auth.getSecurityStatus(); // { twoFactorEnabled, totpEnabled, hasBackupCodes }
await fytkit.auth.regenerateBackupCodes({ password });
await fytkit.auth.disableTwoFactor({ password });

// Password change (verifies the current password server-side). With
// revokeOtherSessions every other session of the account ends, in any app,
// and the SDK adopts the rotated token transparently.
await fytkit.auth.changePassword({ currentPassword, newPassword, revokeOtherSessions: true });

// Email change — a confirmation link is emailed to the new address;
// nothing changes until the trainee opens it.
await fytkit.auth.changeEmail({ newEmail });

// Resend the sign-up verification email to the trainee's own address (there
// is no email input). Resolves to { status: "sent" } or, when the address is
// already verified and nothing is sent, { status: "already_verified" }.
// Throttled per account (one a minute, five an hour): over the limit it
// rejects with { kind: "rate_limited", retryAfter } (milliseconds).
const { status } = await fytkit.auth.resendVerification();

// Devices signed in through this app (plus the calling one); the trainee's
// sessions elsewhere are never listed. revokeId is an opaque id (never the
// bearer token).
const sessions = await fytkit.auth.listSessions();
await fytkit.auth.revokeSession({ revokeId: sessions.find((s) => !s.isCurrent)!.revokeId });
await fytkit.auth.revokeOtherSessions(); // "sign out of your other devices" on this app

// Permanently close the account (re-verifies email + password). On success the
// SDK clears the session and emits signed_out.
await fytkit.auth.closeAccount({ confirmEmail, password });

密码错误或一次性代码为 { kind: "server", status: 400 } 错误 与 code: "invalid_password" 编号 : "invalid_code"; 需要改变 新签注是 { kind: "forbidden", code: "reauth_required" }无 签下受训者——只有401个经认证的电话意味着 课会结束。 密码错误 signIn (例如再次签署) 之后 reauth_required是一个平地 auth_required 离开电流 单独开会。 在Fytkit的训练员上运行或工作的账户不能 通过应用程序和现有应用程序签名、注册或完成两个要素 会话无法更改密码或电子邮件, 无法重新发送其校验邮件或关闭它 : { kind: "forbidden", code: "first_party_account" }指给Fytkit自己看 注册。 两次在程序中注册是 { kind: "conflict" } 与 现有 enrollmentId。 。 。 .

拒绝

当服务器因某种原因拒绝时, 您的应用程序可以采取行动( 教练功能) 计划缺乏,一个已经发送的报到, 一个消息限制,一个工作室是 , 错误带有 refusal: { reason, data } 除了平常的田园. 分会 refusal.reason,从未在消息文本上; refusalData 读取 已知原因的数据已输入 :

import { refusalData } from "@fytkit/sdk";

try {
  await fytkit.messages.send({ body, clientRequestId });
} catch (err) {
  const plan = refusalData(err, "entitlement_required");
  if (plan) return showUpgrade(plan.entitlement);
  const daily = refusalData(err, "daily_limit");
  if (daily) return keepDraftUntil(daily.retryAt);
  throw err;
}

所有原因及其数据: docs/developer/reference/errors.md。 。 。 .

实时聊天

subscribeToMessages 在Supabase 上保持对讲屏 实时: 它获取一个符号( Q)messages.realtimeToken)和fytkit的公众 连接设置( e)app.bootstrap),加入受训者自己的私人 频道(并在实时关闭后重新加入), 刷新符号 并报告何时投票 没有消息内容的事件 : onChange 意思是读取 谈话和未读数".

SDK并不依赖Supabase -js. 添加 @supabase/supabase-js 改为 您的应用程序( y)npm install @supabase/supabase-js@^2.97.0并通过一个工厂 仅为订阅创建一个客户端。 必须是2.87.1或 后:助教用手将学员的道具放入插座上,并 更早的2.x客户端在下个心跳时将它换为肛门键 将订阅保留在 fallback保留 auth 以下选项: 客户端只携带学员的标志,不得保存或刷新 语句,或从页面的 URL 读取一个将替换它的签名 .

import { createClient } from "@supabase/supabase-js";
import { MESSAGES_FALLBACK_POLL_MS, subscribeToMessages } from "@fytkit/sdk";

const unsubscribe = subscribeToMessages(fytkit, {
  realtime: ({ url, anonKey }) =>
    createClient(url, anonKey, {
      auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },
    }),
  onChange: () => refreshConversation(), // messages.list + messages.unreadCount
  onStatus: (status) => {
    // "live": stop polling. "connecting" / "fallback": poll the conversation
    // every MESSAGES_FALLBACK_POLL_MS.conversation (10 s) and the count every
    // MESSAGES_FALLBACK_POLL_MS.unreadCount (60 s). "refused": no trainee to
    // listen for (signed out); the subscription has stopped.
    setPolling(status === "connecting" || status === "fallback");
  },
});

// On leaving the screen or signing out:
unsubscribe();

给大家看"Sam在写作" 而训练队的教练 写回信、通行证 onCoachTyping报告 { kind: "typing", names } (播放姓名,正如谈话显示在他们的信息中)和 { kind: "idle" }。它仅是活的:一个名字本身就消失了8秒 在其最后的标志之后(COACH_TYPING_EXPIRY_MS),所以一个迷失的"停止"从来没有 离开它显示,它去 idle 当频道丢失,而你 民意调查从不显示.

subscribeToMessages(fytkit, {
  realtime,
  onChange: () => refreshConversation(),
  onCoachTyping: (typing) =>
    setWritingLine(typing.kind === "typing" ? `${firstName(typing.names[0])} is writing…` : null),
});

Polling 花费您的应用程序的每分钟请求限制, 所有用户都会使用此限制 共享,所以当状态是 live。完整指南: messages.realtimeToken 和食谱 与实时更新聊天屏幕。 。 。 .

通知偏好

成员切换被存储的服务器侧( 默认适用到第一次写入) :

const { data: prefs } = await fytkit.preferences.getNotifications();
await fytkit.preferences.updateNotifications({ streakAlerts: false }); // partial patch

coachMessages, (中文(简体) ), checkInReminders 财务报告和审定财务报表 coachFeedback 是推动的切换 (直到成员关机为止);“指定程序”按下 newPrograms。 。 。 .

推动通知

Fytkit教练为签约成员注册了设备的"博览会"推克. 代币绑在它注册的会话上 所以签了, 取消该会话或关闭账户会将其删除到服务器上; 呼叫 unregister 首先,所以电话停止 被推一次.

await fytkit.push.register({ token: expoPushToken, platform: "ios" }); // or "android"
await fytkit.push.unregister({ token: expoPushToken }); // idempotent

一个工作室自己的开发者应用被拒绝 { kind: "forbidden" }: 仅 Fytkit教练收到推力. 每一次推车 data 是一个 PushNotificationData (单位:千美元){ v: 1, clientId, kind, href },没有消息文本); 忽略 clientId 工作室不是固定的 合同: 文件/开发者/指南/push-notections.md。 。 。 .

此预览中包含的内容

  • resolveJoinCode (一个工作室与其客户端id,名称,标志和app主题的联想代码,没有客户端)并 app.bootstrap (发射读取,用其ETag重新验证) resolvePaymentMode ("订阅"链接出或"你的计划包括什么",每个商店前端)
  • auth.signUp 编号 : signIn 编号 : signOut 编号 : getSession 编号 : onAuthStateChange
  • auth.signInWithIdToken (与Apple或Google签约,Fytkit教练仅:通过生非ce;Apple获得其SHA-256六,Google获得生非ce)
  • 训练员预览 : auth.startPreview 编号 : getPreview 编号 : onPreviewChange 编号 : endPreview (只读,无成员预览从"app中的预览"QR代码;参见: 训练员预览(中文(简体) )
  • auth.requestPasswordReset 编号 : confirmPasswordReset 编号 : confirmEmail 编号 : resendVerification
  • 双因素: auth.verifyTotp 编号 : verifyBackupCode 编号 : requestEmailOtp 编号 : verifyEmailOtp (签名), enableTotp 编号 : verifyTotpSetup 编号 : disableTwoFactor 编号 : regenerateBackupCodes 编号 : getSecurityStatus (管理层)
  • 账户安全 : auth.changePassword 编号 : changeEmail 编号 : confirmEmailChange 编号 : listSessions 编号 : revokeSession 编号 : revokeOtherSessions 编号 : closeAccount
  • 数据命名空间 : workouts, (中文(简体) ), programs, (中文(简体) ), recipes, (中文(简体) ), nutrition, (中文(简体) ), nutritionLog, (中文(简体) ), profile, (中文(简体) ), preferences, (中文(简体) ), push, (中文(简体) ), articles, (中文(简体) ), enrollments, (中文(简体) ), sessionLogs, (中文(简体) ), progression, (中文(简体) ), progress, (中文(简体) ), bodyMetrics, (中文(简体) ), bodyMeasurements, (中文(简体) ), progressPhotos, (中文(简体) ), checkIns, (中文(简体) ), messages, (中文(简体) ), timeline, (中文(简体) ), health, (中文(简体) ), habits, (中文(简体) ), media, (中文(简体) ), subscription
  • 工作跟踪:程序会话日志和临时工作日志(sessionLogs.create 与 workoutId),设置记录,每个工作历史(sessionLogs.list({ workoutId })(中文(简体) )
  • 进展: progress.getSummary 编号 : getAdherence 编号 : getStreak (存储记录) / getVolume (上周硬套每肌肉组), progression.getPrSummary 编号 : listPrHistory (存储PR历史) / getE1rmTrend (每届估计1-雷普最大,每次升起)/ getHistory, (中文(简体) ), bodyMetrics.list 编号 : record 编号 : update 编号 : delete
  • 教练对记录的会议的反馈: sessionLogs.getFeedback 编号 : markFeedbackSeen 编号 : unseenFeedbackCount
  • 登录 : checkIns.current 编号 : submit 编号 : update 编号 : list 编号 : get 编号 : markReplySeen 编号 : requestPhotoUpload (每周或每两周向教练报到;需要与 coaching_checkins(中文(简体) )
  • 信件数 : messages.list 编号 : send 编号 : requestAttachmentUpload 编号 : markRead 编号 : delete 编号 : unreadCount 编号 : realtimeToken (受训者与导师的对话; 开始一个计划需要与 coaching_messages回答教练不; requestAttachmentUpload 保留随信件一起发送的照片)
  • 实时聊天 : subscribeToMessages (加入学员的私人实时频道 messages.realtimeToken并说何时投票 实时聊天(中文(简体) )
  • 自动信头注射( E)X-TrainerLane-Client-Id, (中文(简体) ), Authorization: Bearer …(中文(简体) )
  • 主动刷新~60 s 在令牌过期前( 下线时用后退) + 被动 401 重试; 只有服务器拒绝刷新标志退出
  • 可插值存储( E)localStorage 默认, SSR 的内存+ cookie)

储存权衡

访问令牌在 localStorage 默认时,名称空格 clientId. 运行在您来源上的任何脚本都可以读取这些脚本——发送严格的CSP,并将SDK像任何其他具有认证的依赖性一样对待. 符號是服务器会话,持续时间可长达7天;签署,关闭账户或撤销开发者app结束其服务器侧.

使用 createMemoryStorageAdapter() 用于SSR+测试.

下一个

  • @fytkit/react ——钩子+ <FytkitVideo>
  • examples/sdk-react ——可运行的样本应用程序
  • docs/sdk/quickstart.md ——全向导

版本与发布记录

@fytkit/sdk 共发布 27 个版本,首次发布于 2026/05/19;过去 12 个月发布了 27 个版本。

版本发布日期距今
5.1.2latest2026/11/123 天前
5.1.12026/11/114 天前
5.1.02026/11/114 天前
5.0.02026/11/079 天前
4.5.02026/11/0114 天前
4.4.02026/10/3115 天前

关于 @fytkit/sdk 的常见问题

@fytkit/sdk 是什么?

用于 fytkit 公共 API 的 Vanilla TypeScript SDK

如何安装 @fytkit/sdk?

在项目目录运行 npm install @fytkit/sdk,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@orpc/client(^1.13.5)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@fytkit/sdk 支持 TypeScript 吗?

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

@fytkit/sdk 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@fytkit/sdk 可以商用吗?

@fytkit/sdk 使用 UNLICENSED 许可。作者声明为 UNLICENSED,即不授予公开使用许可(与公共领域的 Unlicense 不同)。使用前需获得作者授权。以上为概括,不构成法律意见,请以许可原文为准。

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

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

@fytkit/sdk 还在维护吗?

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