- 服务端运行 Node.js 22 或更高版本,使用 ESM。按下方命令固定安装当前组织包;它与旧个人包的版本空间独立。
- 一个已有服务端登录校验的 Web App。示例中的 getSignedInUser 由你的登录系统提供,返回稳定 user.id 或 null。
- 一个 Indiana App,选择 Sandbox,在 SDK 接入页保存 base URL、App ID 和服务端 Key,并设置网站来源。
- Key 至少授权 customers:write、sessions:create、entitlements:read。客户 Token 继承签发 Key 的客户权限。
- 若要看到可用的 generations:保存并启用这个计量功能,再发布含额度的商品,通过 Funnel 领取或购买。只有已配置且符合资格的默认免费套餐/自动试用政策才会分配合同;未配置时空持有是正常结果。
快速开始
从安装 SDK 到读取第一份客户权益。
下载接入示例开始前准备
本例用一个 AI 创作 App 接入客户身份和 generations 权益。示例宿主是 Next.js App Router;SDK 本身不依赖框架。
安装并配置 SDK
本指南使用已发布的 npm 0.0.1 beta,直接执行下面的安装命令,无需构建源码包。下载 ZIP 提供宿主接入文件。在应用根目录创建被 Git 忽略的 .env.local,填写 SDK 接入页显示的值,然后重启开发服务。
浏览器和服务端必须使用同一个 App ID、base URL 和 Sandbox。GROOVE_SECRET_KEY 只出现在服务端模块,绝不能加 NEXT_PUBLIC_ 前缀。
接入当前 App 与 Sandbox
接入 → SDK 接入
当前组件的隔离示例截图;未持久化、未访问真实服务。
准备可信身份与来源
有当前 App apiKeys:write 权限的 Owner、Admin 或 Developer 配置服务端 Key 与 app.example.com 的完整 HTTPS 来源。后端识别自己的用户,浏览器只接收 customer token。
完成后检查真实 App 的进度由持久事实决定;示例空状态不代表服务已连通。
完成后检查
依赖已安装,环境变量已填写,Secret Key 没有出现在任何浏览器代码中。
# 安装文档使用的 npm 0.0.1 beta 版本npm install @tinkerlandai/indiana-sdk@0.0.1建立服务端客户端
新建 lib/groove-server.ts。这个模块集中保存 Key 和范围,供你的会话接口和受保护的业务操作使用。
用户身份以你自己的登录系统为准。相同 App、环境、externalUserId 再次识别会返回同一个客户,不按邮箱合并,也不覆盖已有资料。
完成后检查
缺少配置时模块明确报错;Key 只在服务端读取。下一步会使用真实登录会话识别客户。
import "server-only";import { createGrooveServer } from "@tinkerlandai/indiana-sdk/server"; function required(name: string): string { const value = process.env[name]; if (!value) throw new Error(`Missing ${name}`); return value;} export const groove = createGrooveServer({ baseUrl: required("GROOVE_BASE_URL"), apiKey: required("GROOVE_SECRET_KEY"), scope: { projectId: required("GROOVE_PROJECT_ID"), environment: "sandbox", },});为当前登录用户签发会话
在你自己的应用里新增 GET /api/groove-session。它先校验登录,再识别客户,最后返回这个客户的短期 Token。浏览器不提交 customerId 或 externalUserId。
将 @/lib/auth 替换为你现有的服务端认证模块。第二个选项卡只说明它的类型约定,需要你提供真实实现。
完成后检查
已登录请求返回 customerToken 和 expiresAt,响应为 no-store;未登录请求返回 401。Token 默认 10 分钟过期,权限每次请求重新核对。
import { groove } from "@/lib/groove-server";import { getSignedInUser } from "@/lib/auth"; export async function GET(request: Request) { // 使用你现有的服务端登录校验。不要从查询参数取用户 ID。 const user = await getSignedInUser(request); if (!user) return Response.json({ error: "Unauthenticated" }, { status: 401 }); const { customer } = await groove.getOrCreateCustomer({ externalUserId: user.id, }); const { customerToken, expiresAt } = await groove.createCustomerSession(customer.id); return Response.json( { customerToken, expiresAt }, { headers: { "Cache-Control": "no-store" } }, );}在浏览器读取并显示权益
新建 lib/groove-client.ts,再将 GenerationAccess 放进你的已登录页面。先获取会话并 identify,然后读取当前客户的权益。
持有按服务端时间和实际账本计算。accessible、unlimited 和 available 用于界面展示;真正执行生成任务时,仍需在可信后端授权并原子消费或预占积分。
完成后检查
页面显示 generations 可用或不可用;浏览器网络请求携带客户 Token,代码里没有 Secret Key。登出前调用 grow.reset()。
import { createGroove } from "@tinkerlandai/indiana-sdk"; export const grow = createGroove({ baseUrl: process.env.NEXT_PUBLIC_GROOVE_BASE_URL!, scope: { projectId: process.env.NEXT_PUBLIC_GROOVE_PROJECT_ID!, environment: "sandbox", },}); export async function connectGroove(signal?: AbortSignal) { const response = await fetch("/api/groove-session", { credentials: "same-origin", cache: "no-store", signal, }); if (!response.ok) throw new Error(`Session failed: ${response.status}`); const { customerToken } = await response.json(); if (typeof customerToken !== "string") throw new Error("Invalid session"); signal?.throwIfAborted(); grow.identify(customerToken); return grow.readHoldings();} // 登出或切换用户前调用;不要把 Token 存入 localStorage。export function disconnectGroove() { grow.reset();}验证接入与常见问题
| 现象 | 检查方法 |
|---|---|
| UNAUTHENTICATED | 先确认宿主会话接口返回 200;Token 过期时重新签发一次,再重试。不要无限重试。 |
| FORBIDDEN | Key 是否有 entitlements:read?是否已撤销、过期或缩减权限? |
| SCOPE_MISMATCH | 前后端的 App ID 和环境是否一致?Header 不能切换 Key 的范围。 |
| unassigned / allowed: false | 是否已发布并通过 Funnel 授予商品?权益 key 是否为 generations?已有客户保留其原合同版本。 |
| 浏览器跨域失败 | SDK 接入里的来源是否与网站协议、域名和端口完全一致? |
从网络面板和控制台核对
UNAUTHENTICATED
- 检查方法
- 先确认宿主会话接口返回 200;Token 过期时重新签发一次,再重试。不要无限重试。
FORBIDDEN
- 检查方法
- Key 是否有 entitlements:read?是否已撤销、过期或缩减权限?
SCOPE_MISMATCH
- 检查方法
- 前后端的 App ID 和环境是否一致?Header 不能切换 Key 的范围。
unassigned / allowed: false
- 检查方法
- 是否已发布并通过 Funnel 授予商品?权益 key 是否为 generations?已有客户保留其原合同版本。
浏览器跨域失败
- 检查方法
- SDK 接入里的来源是否与网站协议、域名和端口完全一致?
相关:完整代码示例·积分消费·HTTP 参数与响应