npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@neoaira/scoutaro-sdk

v0.1.3

Published

Scoutaro browser API client and embedded registration widget

Readme

Scoutaro 浏览器 SDK 接入手册

Scoutaro SDK 为落地页提供完整的 Google 登录与内测报名区域:加载登录按钮、按需收集建议和隐私确认、按服务端配置执行 Turnstile、提交报名,以及恢复已预约状态。纯 TypeScript / 原生 DOM 实现,不依赖 React 或 Vue;可用于静态 HTML、React、Vue、Astro 等浏览器页面。

这份文档是 SDK 对外接入的独立维护入口。npm 包名为 @neoaira/scoutaro-sdk,采用 MIT 许可,提供 npm 安装、本地包安装和自托管 ES Module 接入方式。没有官方公共 CDN;不要把示例 API 地址当成公共托管服务。MIT 许可仅覆盖 SDK 及其发布产物,不授予本仓库 API 和管理后台的使用许可。

目录

1. 快速接入

准备一个用于容纳 SDK 的 DIV。将 SDK 的 dist/scoutaro.js 放到网站的 /assets/scoutaro.js 后,在页面添加:

<div id="registration"></div>
<script type="module">
  import { mountRegistration } from '/assets/scoutaro.js';

  const widget = mountRegistration({
    container: document.querySelector('#registration'),
    apiBase: 'https://your-public-api.example.com',
    slug: 'your-product',
  });
  widget.ready.catch((error) => {
    // 可用于接入诊断。保留组件,SDK 的入口支持用户点击重试。
    console.warn('Scoutaro initialization:', error.code);
  });
</script>

页面初始化只显示一个“使用 Google 预约内测”按钮,配置加载完成后也不会自动展开面板。用户点击入口后,SDK 展示 Google 官方登录按钮,用户点击该按钮完成登录。此流程不通过模拟点击或隐藏覆盖层触发 Google 登录。

默认不显示隐私确认和功能建议。用户完成登录后,SDK 按需执行 Turnstile,然后直接报名。成功后显示白底细边框的“已预约”状态按钮和已验证邮箱;按钮不可重复提交,不再展示资格卡片。初始化不会自动登录或报名。

不需要接入方自己加载 Google / Turnstile 脚本、获取第三方 token 或编写报名请求。公开 Key 由 Scoutaro 的 config 接口提供,Secret Key 不进入页面。

2. 接入前准备

| 配置 | 由谁设置 | 用途 | | ----------------------- | ----------------------- | ------------------------------------------------------------ | | 公共 API 地址 apiBase | Scoutaro 服务维护者 | 提供 config、stats、applications、application 四个接口 | | 产品标识 slug | Scoutaro 后台创建产品 | 唯一标识,如 replybridge,不是产品 UUID | | 产品允许来源 | Scoutaro 后台产品编辑页 | 登记落地页准确的协议、主机和端口 | | Google Client ID | Scoutaro 后台产品编辑页 | 当前产品 Client ID;SDK 自动读取公开值 | | Google JavaScript 来源 | Google OAuth Web 客户端 | 必须与实际打开落地页的 Origin 对应 | | Turnstile | Scoutaro 后台产品编辑页 | 逐产品控制启用和配对 Key,组件登记该产品 hostname | | 隐私条款 | Scoutaro 后台产品条款页 | 后台开启“要求隐私确认”时检查条款;未自定义的语言使用系统默认 |

本地示例:API 为 http://127.0.0.1:8787,SDK 示例页面为 http://127.0.0.1:8792,产品标识为 scoutaro-local-test。在产品允许来源和 Google 控制台登记的是 8792 页面来源,不是 8787 API 来源。localhost 与 127.0.0.1 是不同来源;端口也必须匹配。

生产页面通过 HTTPS 提供 SDK 文件并连接 HTTPS API。浏览器自动附带 Origin;不要在页面代码中伪造该请求头。API 只接受产品已经登记的来源。

服务维护者可参考仓库的运维说明配置 Google、Turnstile、JWT 和后台身份服务;SDK 使用者只需取得 API 地址、产品标识以及页面来源授权。

3. 初始化参数与完整流程

mountRegistration({
  container, // 必填:已挂载的 HTMLElement
  apiBase: 'https://api.example.com',
  slug: 'my-product',
  featureRequest: false, // 可选:默认 false
  locale: 'zh-CN', // 可选:省略时优先页面语言,再浏览器语言
  storage: null, // 可选:null 表示仅内存;省略由 SDK 自动持久化并跨标签页恢复
  nonce: 'server-generated-nonce', // 可选:有 CSP nonce 要求时传入
  onSuccess({ google_email }) {
    /* 服务端确认报名后调用 */
  },
});

| 参数 | 类型 | 默认值/说明 | | --------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------ | | container | HTMLElement | 必填。SDK 仅管理自己追加的区域 | | apiBase | string | 必填。公共 API 的基础地址,不带 /api/products/... 路径 | | slug | string | 必填。后台创建的产品唯一标识 | | privacyConfirmation | boolean | 已弃用,传入值不生效;由后台产品的 privacy_enabled 控制 | | featureRequest | boolean | false。是否展示选填建议,最长 2000 字符 | | locale | string | 默认先读页面语言,再读浏览器首选语言;显式传入不支持的语言会报错 | | storage | Storage 子集或 null | 默认 localStorage,受限时降级;自定义存储须提供同步的 getItem/setItem/removeItem | | nonce | string | 无。用于 SDK 样式和动态加载的第三方脚本 | | onSuccess | (result: { google_email: string }) => void | 无。报名 POST 成功或恢复已有报名的 POST 成功后调用 |

featureRequest 必须使用布尔值 true / false,默认关闭。隐私确认请在后台“产品管理 → 创建产品/编辑产品 → 隐私确认 → 要求隐私确认”设置,默认开启,保存后刷新落地页生效。初始化参数不可原地修改;改变建议选项时销毁并重新挂载,语言切换使用 setLocale()。

| 隐私确认 | 功能建议 | 登录前展示与操作 | | -------- | -------- | ---------------------------------------- | | 关闭 | 关闭 | 只展示 Google 登录按钮 | | 开启 | 关闭 | 按钮下展示条款,勾选同意后才可登录 | | 关闭 | 开启 | 按钮下展示选填建议,可留空后登录 | | 开启 | 开启 | 按钮下展示建议和条款,勾选同意后才可登录 |

所有组合均在 Google 登录成功后,按需执行 Turnstile 并直接提交,不再要求点击“确认预约”。未勾选时 Google 授权被阻止;点击登录区域或用键盘激活会显示当前语言的同意提示,并聚焦勾选框。勾选后提示消失,才能登录;回调也会再次检查同意状态。提交失败才显示“重试”;条款变化会重新加载、清空勾选并要求再次登录。

**隐私开关的实际含义:**关闭时不展示条款、不要求确认;即使后台保存了条款,也不会阻止该次报名或保存条款快照。开启时,SDK 从后台获取条款并要求明确勾选,条款缺失则不开放报名;服务端核对摘要并保存首次快照。

隐私确认由后台产品的 privacy_enabled 唯一控制。SDK 从 config 读取开关,API 也读取数据库并强制校验。旧 privacyConfirmation 参数和请求中的 privacy_enabled 字段仅保留兼容,不能覆盖后台配置。管理员在提交过程中切换开关时,SDK 会重新加载配置,不自动再次提交;开启后必须重新勾选并登录。关闭不删除条款自定义内容或历史报名快照。Google 身份、来源权限和 Turnstile 校验仍执行。

**功能建议的实际含义:**关闭时 SDK 不发送 feature_request,新记录保存为 null;开启后仍然选填。重复报名不会覆盖第一次提交的建议。

4. 可运行的 HTML 示例

示例文件使用 ../dist/scoutaro.js,可连同 dist/ 一起通过 HTTP 静态服务器提供。不要直接双击 HTML 使用 file:// 打开。

从 Scoutaro 仓库根目录执行:

pnpm dev

该命令同时启动 SDK 调试页面(8792)、API(8787)和后台(8788),并显示地址与 API 状态;首次运行先按根目录 README 迁移本地数据库。如果示例服务已经运行,新增示例路由后需要重启一次。仅开发 SDK 或运行模拟回归时,也可以单独执行 pnpm --filter @neoaira/scoutaro-sdk dev,但不要与根目录 pnpm dev 重复启动。示例是真实服务接入示例,点击登录和提交会在所选产品下产生报名;与 /__tests 的模拟回归页面不同。

示例 A:最简预约

文件:examples/basic.html

地址:http://127.0.0.1:8792/examples/basic.html

const widget = mountRegistration({
  container: document.querySelector('#registration'),
  apiBase: 'http://127.0.0.1:8787',
  slug: 'scoutaro-local-test',
});
widget.ready.catch((error) => console.warn(error.code));

默认优先页面 html.lang,再使用浏览器语言。隐私确认遵循后台开关;开启时先同意条款,Google 登录后直接报名,成功按钮包含邮箱。没有自定义条款时使用系统默认正文。

示例 B:隐私确认+功能建议

文件:examples/form.html

地址:http://127.0.0.1:8792/examples/form.html

const widget = mountRegistration({
  container: document.querySelector('#registration'),
  apiBase: 'http://127.0.0.1:8787',
  slug: 'scoutaro-local-test',
  featureRequest: true,
});
widget.ready.catch((error) => console.warn(error.code));

先在后台开启产品的“要求隐私确认”,检查默认或自定义条款。Google 按钮下方展示建议和条款:填写建议或留空,阅读条款并勾选,再点击 Google 登录。登录成功后直接报名。条款在填写期间变化时,SDK 重新加载条款并清空勾选,用户重新确认并登录后才能提交。

示例 C:只确认隐私

文件:examples/privacy.html

地址:http://127.0.0.1:8792/examples/privacy.html

后台开启“要求隐私确认”,SDK 不启用功能建议。用户阅读条款并勾选同意后,通过 Google 登录自动报名。

示例 D:品牌主题+建议+统计

文件:examples/themed.html

地址:http://127.0.0.1:8792/examples/themed.html

此示例使用深绿色背景和独立配色变量,启用建议;隐私确认仍遵循后台开关。页面首次进入与报名成功后主动调用 getStats() 更新人数。统计失败显示暂不可用,不把失败误报为零人,也不会改变已经成功的报名状态。

四个示例默认连接同一个测试产品,因而会共享该产品的已预约会话,包括同一浏览器、同一来源的新标签页。要比较未报名流程,可清除该产品对应的本地会话(localStorage 与旧 sessionStorage)后刷新。清除会话不会删除服务端报名;同一 Google 账号再次报名仍会去重。

5. React 与 Astro 示例

先按打包说明安装本地 SDK 包。React 组件使用客户端 effect 挂载,卸载时释放:

import { useEffect, useRef } from 'react';
import { mountRegistration } from '@neoaira/scoutaro-sdk';

export function Registration({ apiBase, slug }: { apiBase: string; slug: string }) {
  const container = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!container.current) return;
    const widget = mountRegistration({
      container: container.current,
      apiBase,
      slug,
      featureRequest: true,
    });
    void widget.ready.catch((error) => console.warn('Scoutaro:', error.code));
    return () => widget.destroy();
  }, [apiBase, slug]);

  return <div ref={container} />;
}

React Strict Mode 在开发时会执行 effect 的清理与重建;必须保留 destroy()。不要把组件实例保存到模块级变量,也不要在 render 或服务端渲染阶段调用 mountRegistration()。

Astro 页面可以复用上面的 React 组件:

---
import { Registration } from '../components/Registration';
---
<Registration
  client:load
  apiBase="http://127.0.0.1:8787"
  slug="scoutaro-local-test"
/>

Astro 页面通常使用不同端口,例如 4321。应在 Scoutaro 产品与 Google 控制台登记实际 Astro 页面来源,而不是照抄 SDK 示例的 8792。

6. Vue 3 示例

<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { mountRegistration, type RegistrationWidget } from '@neoaira/scoutaro-sdk';

const container = ref<HTMLElement | null>(null);
let widget: RegistrationWidget | undefined;

onMounted(() => {
  if (!container.value) return;
  widget = mountRegistration({
    container: container.value,
    apiBase: 'http://127.0.0.1:8787',
    slug: 'scoutaro-local-test',
    featureRequest: true,
  });
  void widget.ready.catch((error) => console.warn('Scoutaro:', error.code));
});
onBeforeUnmount(() => widget?.destroy());
</script>

<template><div ref="container" /></template>

该示例的产品与功能开关固定。如果把它们改为可变 props,应在变化时先 destroy(),再重新挂载;不要在同一个容器里叠加多个实例。

7. 生命周期与回调

mountRegistration() 返回:

| 成员 | 返回值 | 行为 | | ------------------- | --------------- | --------------------------------------------------------------------- | | ready | Promise<void> | 首次配置加载、已有会话检查及登录入口准备完成;初始化失败会 reject | | setLocale(locale) | Promise<void> | 切换语言并重新读取配置;保留尚未提交的建议,清除隐私勾选,不自动提交 | | destroy() | void | 取消请求与挑战、释放 Google 回调归属、移除 SDK 添加的 DOM;可重复调用 |

ready 不是“报名已完成”的通知,也不跟踪用户之后的所有重试。初始化失败时入口仍保留,接入页面不要在 catch 中移除或隐藏整个组件。

const widget = mountRegistration({
  container: document.querySelector('#registration'),
  apiBase,
  slug,
  onSuccess({ google_email }) {
    // 这里只表示服务端已经确认报名,可触发页面自己的反馈。
    document.querySelector('#feedback').textContent = '报名已确认';
    void refreshStats().catch(() => {
      // 统计故障不撤销报名,也不重新提交。
    });
  },
});
void widget.ready.catch((error) => console.warn(error.code));
// 页面语言控件触发:
// await widget.setLocale('ja');
// 路由离开时:
// widget.destroy();

onSuccess 不包含 Google ID Token。刷新页面后通过查询 JWT 只读恢复已预约状态时,不触发 onSuccess。回调中如有异步工作,应自行捕获 Promise 错误。销毁无法撤销服务端已经保存的报名。

成功区域只显示“已预约”和邮箱,不显示内测资格,也不提供“切换账号”按钮。需要获取资格时可独立使用 getApplication()。Google 登录时由用户选择账号。

8. 配色与页面布局

SDK 外层透明,无卡片背景、边框、阴影和内边距。继承容器的字体家族与文字颜色;SDK 保持表单字号层级。区域最大宽度为 460px,Google 按钮及成功状态按钮最大宽度为 400px。外围背景、位置、居中方式和留白由页面控制。

#registration {
  max-width: 460px;
  margin-inline: auto;
  color: #e2e8f0;
  font-family: inherit;
  --scoutaro-control-bg: #1e293b;
  --scoutaro-control-text: #e2e8f0;
  --scoutaro-border: #64748b;
  --scoutaro-primary: #a7f3d0;
  --scoutaro-on-primary: #064e3b;
  --scoutaro-focus: #67e8f9;
  --scoutaro-error: #fca5a5;
}

| CSS 变量 | 作用 | | ------------------------- | ------------------------------------ | | --scoutaro-control-bg | 输入框、普通操作按钮和已预约按钮背景 | | --scoutaro-control-text | 上述控件的文字颜色 | | --scoutaro-border | 控件边框和表单分隔线 | | --scoutaro-primary | 确认按钮背景及勾选框强调色 | | --scoutaro-on-primary | 确认按钮文字颜色 | | --scoutaro-focus | 键盘焦点轮廓 | | --scoutaro-error | 提交错误文案颜色 |

不传变量时使用默认浅色控件。变量不修改 Google 官方 iframe 或 Turnstile 内部界面。成功状态使用包内 Google 图标,按“已预约:邮箱”同行展示,长邮箱自动换行;邮箱来自服务端验证结果。此状态不是可点击的 Google 登录按钮。

SDK 不使用 Shadow DOM,页面过强的全局选择器仍可能覆盖内部样式。建议把宿主的 button、form、summary 等规则限制在自身组件范围内;无需复制或覆盖 SDK 整份 CSS。

9. 多语言与条款

SDK 和公共接口支持以下 20 个语言选项。选择口径为全球网站用户覆盖,不代表严格的语言人口排名。

| 代码 | 语言 | 代码 | 语言 | | ----- | -------- | ----- | ------------ | | zh-CN | 简体中文 | zh-TW | 繁体中文 | | en | 英语 | es | 西班牙语 | | ja | 日语 | de | 德语 | | fr | 法语 | pt | 葡萄牙语 | | ru | 俄语 | it | 意大利语 | | ko | 韩语 | ar | 阿拉伯语 | | hi | 印地语 | id | 印度尼西亚语 | | vi | 越南语 | tr | 土耳其语 | | th | 泰语 | nl | 荷兰语 | | pl | 波兰语 | uk | 乌克兰语 |

优先级:显式 locale → 页面 html.lang → navigator.languages 中第一个支持的语言 → 简体中文。兼容 navigator.language。对应语言未自定义时由 API 返回该语言的系统默认条款,不要求接入方手动复制模板。阿拉伯语使用 RTL。

SDK 文案、成功状态和错误文案内置翻译;通过 Accept-Language 与服务端协商错误及条款语言。Google 按钮传入官方 locale;SDK 自行加载的 Google 脚本会随语言切换更新 hl,切换期间保留加载入口。若页面自行预加载 GIS,脚本语言由页面负责,应避免重复加载;账号选择器自身语言仍由 Google / 浏览器控制。

隐私条款由管理员逐语言填写纯文本,保存为 JSON。SDK 不翻译条款正文或用户建议。缺少当前页面语言时返回 null,开启隐私确认时阻止首次报名;正文只按文本渲染。

启用确认时,SDK 提交实际条款的 locale 和 digest,服务端在首次报名中保存正文、语言和摘要。摘要用于防止阅读后内容变化,不是后台版本管理编号。关闭确认时不展示、不提交这些信息,快照保持空。重复报名不会补写或改写第一次快照。

10. 纯 API 客户端

需要完全自定义 UI 时使用 ScoutaroClient。它只封装 API、语言和查询 JWT,不会渲染 Google / Turnstile 或自动收集第三方 token。希望 SDK 全部处理时使用 mountRegistration()。

import { ScoutaroClient } from '@neoaira/scoutaro-sdk';
const client = new ScoutaroClient({ apiBase, slug, locale: 'zh-CN' });
const stats = await client.getStats();
console.log(stats.reservation_count);

| 方法 | HTTP 接口 | 返回值 | | ----------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------- | | getConfig(signal?) | GET /api/products/{slug}/config | ProductConfig:公开 Key、产品人机验证开关、语言与条款 | | getStats(signal?) | GET /api/products/{slug}/stats | Statistics:报名总数、内测人数、配额、候补及剩余名额 | | submitApplication(input, signal?) | POST /api/products/{slug}/applications | ApplicationResult:token、expires_in、google_email;自动保存查询 JWT | | getApplication(signal?) | GET /api/products/{slug}/application | ApplicationStatus:registered、eligible、google_email | | setLocale(locale) | 无请求 | 修改后续请求使用的语言 | | clearSession() | 无请求 | 清除本地查询 JWT;不退出 Google、不删除报名 |

四个业务方法均返回 Promise;可选 signal 为 AbortSignal。setLocale() 和 clearSession() 是同步生命周期操作。

config 与条款类型

interface PrivacyPolicy {
  locale: Locale; // 实际返回的条款语言
  content: string; // 纯文本正文
  digest: string; // locale + 正文的服务端摘要,提交时原样带回
}
interface ProductConfig {
  product_id: string;
  google_client_id: string; // 公开值
  privacy_enabled: boolean; // 后台产品设置,默认开启
  turnstile_enabled: boolean;
  turnstile_site_key: string; // 公开值;没有 Secret Key
  locale: Locale; // 本次协商语言
  default_locale?: Locale; // 已弃用的兼容字段,不是产品设置
  supported_locales: Locale[];
  privacy_policy: PrivacyPolicy | null;
}

API 对未自定义的语言返回系统默认条款。若异常配置返回 privacy_policy: null,后台开启隐私确认时 SDK 会阻止报名。privacy_enabled 和 turnstile_enabled 都由后台产品设置控制,接入页面不能通过组件参数关闭。

请求字段

interface ApplicationInput {
  google_credential: string;
  privacy_enabled?: boolean; // 已弃用,服务端忽略;新代码不发送
  privacy_confirmed?: boolean;
  privacy_locale?: Locale;
  privacy_digest?: string;
  feature_request?: string | null;
  turnstile_token?: string;
}
  • google_credential:Google 返回的 ID Token,不是 Client ID、Google access token 或 Scoutaro 查询 JWT;每次提交都由后端验签。
  • privacy_enabled 请求字段:已弃用,服务端忽略,新 SDK 不发送。请读取 config.privacy_enabled,它由后台产品设置控制。
  • privacy_confirmed:开启隐私确认时首次报名必须为 true。
  • privacy_locale / privacy_digest:开启确认且产品有条款时,从 config.privacy_policy 原样复制;正文变化返回 PRIVACY_CHANGED。
  • feature_request:选填,最多 2000 字符;去掉首尾空白,空内容保存为 null。SDK 关闭建议时不发送它。
  • turnstile_token:服务端配置启用 Turnstile 时首次报名必填;token 一次性使用,失败后重新获取。

后台产品已关闭确认时的纯客户端示例(先读取 config 确认;Google token 和挑战需由调用方获取):

const result = await client.submitApplication({
  google_credential: googleIdToken,
  ...(turnstileToken ? { turnstile_token: turnstileToken } : {}),
});
// result.google_email 为后端验证的邮箱。

常用返回值

interface Statistics {
  reservation_count: number; // 有效报名总人数
  beta_count: number; // min(报名人数, 配额)
  beta_quota: number; // 内测配额
  waitlist_count: number; // max(报名人数 - 配额, 0)
  remaining_count: number; // max(配额 - 报名人数, 0)
}
interface ApplicationStatus {
  registered: boolean;
  eligible: boolean;
  google_email: string | null;
}
interface ApplicationResult {
  token: string;
  expires_in: number; // 秒
  google_email: string;
}

类型声明随包提供,还导出 Locale、ClientOptions、WidgetOptions、RegistrationWidget、ProductConfig、PrivacyPolicy、ScoutaroError。SDK 不自动获取统计;按页面需要自行调用。

11. 会话、错误与排查

查询 JWT 由 SDK 默认保存在当前站点的 localStorage,键按 API 地址与产品隔离。相同浏览器、相同来源的新标签页或重新打开页面,会在有效期内携带凭据调用 API 恢复已预约状态与邮箱;宿主页面无需实现存储。SDK 不单独持久保存邮箱或已预约标记,也不自动重复提交报名。

升级后首次读取会将当前标签页旧 sessionStorage 查询会话迁移到 localStorage,保留原过期时间。已关闭标签页的旧会话无法恢复;再次用相同 Google 账号登录时,服务端按既有规则去重。localStorage 不可用时降级到 sessionStorage,两者均不可用时退回内存。过期、服务端 401 或 clearSession() 会清除默认存储中的当前产品会话;临时网络故障保留凭据。

storage: null 关闭持久化;自定义 storage 时完全由指定存储负责,SDK 不读取或迁移默认浏览器存储。Google ID Token 和 Turnstile Token 始终只在内存中保留。此改动不延长服务端 JWT 有效期(默认 24 小时),也不能跨浏览器、隐私窗口、根域与 www 等不同来源共享身份。

刷新时,有有效查询 JWT 就请求本人状态并恢复“已预约+邮箱”,不会重新 POST。JWT 默认有效期为 24 小时,实际以服务端 expires_in 为准;没有自动续期,过期后用户重新 Google 登录。服务端按产品与 Google 身份去重,重复提交不会重复计数或通知。

遇到 TURNSTILE_REQUIRED、TURNSTILE_FAILED 或 TURNSTILE_UNAVAILABLE 时,组件每次失败最多重新获取一次产品配置,然后等待用户主动重试;不会自动再次提交报名。重试按最新开关和 Site Key 创建新挑战。建议内容保留;Google Client ID 变化时要求重新登录,条款摘要或语言变化时清空同意勾选。配置刷新失败时,重试先重新加载配置,不能继续使用旧配置提交。配置未变化时仍显示原错误,持续失败应检查服务配置。

SDK 每个 API 请求超时为 15 秒。网络失败、503 或 401 不应被接入页面解读为未报名。提交超时可能意味着响应丢失;SDK 不自动重试写入,用户主动重试时由服务端去重。

错误类型 ScoutaroError 提供 code、message、status、requestId。按稳定 code 判断,不按翻译后的 message 判断。传给 onSuccess 的只有邮箱,业务 API 的完整结果可通过纯客户端获取。

| 现象/错误 | 排查方式 | | ----------------------------------------- | ------------------------------------------------------------------------------- | | 初始入口有显示,但点了无法登录 | 检查 API、产品 slug、Google Client ID、浏览器网络请求;ready 可读取初始化错误 | | ORIGIN_NOT_ALLOWED | 产品后台登记实际页面 Origin;注意 localhost、127.0.0.1 和端口区别 | | Google 来源错误 | 在 Google Web OAuth 客户端登记页面 JavaScript Origin;当前使用弹窗回调流程 | | PRIVACY_CHANGED | 阅读期间条款更新;重新加载并再次勾选,禁止自动同意 | | TURNSTILE_REQUIRED / TURNSTILE_FAILED | 检查产品开关、对应的 Key、允许 hostname;重试时重新获取挑战 | | INVALID_GOOGLE_TOKEN | 重新登录;检查 Google Client ID 与服务端验签受众一致 | | GOOGLE_BUSY | 同一页面已挂载另一个完整组件;先销毁旧组件 | | 刷新仍显示已预约 | 正常会话恢复;清除会话不会删除服务端记录 | | 修改演示后台后 API 没变化 | 本仓库 8790 演示库与 8787/8788 开发库独立 | | 新 SDK 报 INVALID_INPUT | 确认 config 已返回布尔型 privacy_enabled,先升级服务端 |

CSP 限制页面需要传入对应 nonce,并允许 API 地址的 connect-src,以及 Google / Turnstile 官方要求的 script、frame、connect 来源。初始化入口使用包内 PNG data URL 图标,img-src 需允许 data:。SDK 无法绕过宿主 CSP、浏览器弹窗限制或第三方登录限制。

同页同时只允许一个完整报名组件;同页 Google Client ID 固定,不应与另一套 GIS 初始化混用。可以创建多个纯 API 客户端。SDK 使用 Google 弹窗登录,不实现整页 OAuth 重定向;真实手机、内置浏览器和第三方 Cookie 设置需在目标环境验收。

12. 打包、升级与验证

本地包安装

在 Scoutaro 仓库根目录:

pnpm --filter @neoaira/scoutaro-sdk build
pnpm --filter @neoaira/scoutaro-sdk pack --pack-destination /tmp/scoutaro-sdk-package

把生成的包复制到落地页仓库的 vendor/ 中,然后在落地页项目中执行(版本号以产物为准):

npm install ./vendor/neoaira-scoutaro-sdk-0.1.0.tgz

落地页应保存 vendor 包和依赖锁文件。包内包含翻译和独立类型声明,不需安装 @scoutaro/i18n 或引用相邻 Scoutaro 源码。pack 会自动重新构建;发布白名单只包含 JS、类型声明、示例、README、LICENSE 和包清单,不包含 source map 或原始 TypeScript。

npm 公共包

在接入项目执行:

pnpm add @neoaira/scoutaro-sdk
import { mountRegistration } from '@neoaira/scoutaro-sdk';

apiBase 填写自己的 Scoutaro API 地址。公开 SDK 不会将报名数据发送给 npm 或 Neoaira。

维护者在仓库根目录先执行 pnpm test:sdk-package,验证实际 tarball 可以在仓库外离线安装、导入和通过类型检查。发布前完成 pnpm check、pnpm format:check,提交发布准备改动,再按下节通过 GitHub Release 发布。无需在本机登录 npm 或上传包。

只有 SDK 包公开,主仓库和其他 workspace 包保持私有。每次发布使用新的版本号;发布完成后应验证 npm view @neoaira/scoutaro-sdk version --registry=https://registry.npmjs.org/,实际可用版本以 npm registry 为准。

通过 GitHub Release 发布

正式发布入口统一为 GitHub Release,工作流为 .github/workflows/publish-sdk.yml,Actions 页面显示为 Publish SDK。合并到 main 只运行 CI,不再自动发布 npm。工作流使用既有 npm Trusted Publishing(OIDC),不需要保存 npm Token;组织 neoaira、仓库 scoutaro、工作流文件 publish-sdk.yml 的绑定保持不变。

下一版以 0.1.2 为例:

  1. 修改 apps/sdk/package.json 的 version 为 0.1.2,将 SDK 改动、测试和说明合并到 main,确认 CI 通过。
  2. 打开 GitHub → neoaira 组织 → scoutaro 仓库 → Code → 右侧 Releases → Draft a new release(第一次可能显示 Create a new release)。
  3. 点击 Choose a tag → 输入 v0.1.2 → Create new tag,Target 选择已通过 CI 的 main 提交。标签必须是 v 加包版本号,且指向已合入 main 的提交。
  4. 填写标题 Scoutaro SDK v0.1.2 和更新说明,保持 Set as a pre-release 未勾选,点击 Publish release。保存草稿、只推送标签、编辑既有 Release 均不触发正式发布。
  5. 进入仓库 Actions → Publish SDK → 对应 Release 的运行记录。流程检出标签指向的代码,核对标签与包版本、格式、类型、测试、构建、独立安装和持久化,再发布到 npm。新版本必须高于 npm latest;已存在的版本安全跳过,不覆盖。
  6. 等待 npm 完成处理后执行 npm view @neoaira/scoutaro-sdk version --registry=https://registry.npmjs.org/,确认新版本可安装。GitHub Release 已发布不等于 npm 流程已经成功;应检查 Actions 结果。

**仅演练:**仓库 Actions → Publish SDK → Run workflow → 选择 main → 填写当前包版本 → Run workflow。此入口始终只测试和打包,不能正式发布;已有版本跳过 npm 发布预检。正式发布失败后,在对应 Release 的 Actions 运行记录选择 Re-run failed jobs 或 Re-run all jobs,不要移动已经发布的标签或覆盖 npm 版本。不要重新运行历史旧版发布工作流以绕过当前入口。

旧变量 SDK_AUTO_PUBLISH 已不再使用。只支持 vX.Y.Z 正式 Release,预发布不会发布 npm。当前仓库私有,GitHub Release 也仅仓库有权限的成员可见;npm 包仍为公开包。工作流关闭 provenance 生成,不影响既有 OIDC 认证。Release 事件与检出规则见 GitHub 官方说明。

v0.1.1 是为已发布 npm 包补建的历史 Release,标签固定指向实际发布提交 7af268ee69eff532f4904db154a773a0d05ac4b0,不重新上传 npm;新流程从后续版本开始使用。

静态文件接入

将 dist/scoutaro.js 放到自己站点静态目录,通过 <script type="module"> 导入。不要把文件当普通脚本期待出现 window.Scoutaro;当前产物是 ES Module,没有 UMD 全局入口。自建 CDN 时需正确设置 JavaScript MIME、CORS 和版本化缓存。

从旧版升级

  1. 先应用 0008_product_privacy_confirmation.sql(现有产品默认开启),再更新 API 和后台,最后升级 SDK 与重建落地页。已发布迁移保持不变。
  2. 隐私确认在后台产品设置管理;功能建议仍通过 featureRequest 控制。旧 SDK 的界面仍受旧参数影响,必须升级才能跟随后台开关;旧请求参数不能绕过新版 API 校验。
  3. 成功状态改为“已预约+邮箱”;若页面需要资格或人数,使用纯客户端独立查询。
  4. 重新构建落地页;如果使用复制的压缩包,仅重新构建 Scoutaro 不会更新落地页中的旧副本。
  5. 检查四种开关组合、有效会话恢复、语言切换、失败重试以及手机排版。

开发与回归

pnpm --filter @neoaira/scoutaro-sdk typecheck
pnpm --filter @neoaira/scoutaro-sdk build
pnpm --filter @neoaira/scoutaro-sdk dev
pnpm check

http://127.0.0.1:8792/__tests 提供模拟 Google、Turnstile 和 API 的浏览器回归,不写真实报名。它覆盖 20 种语言、RTL、选项组合、确认门槛、重复回调、会话恢复、错误重试和销毁。

维护接口或默认值时,必须同步更新本手册、HTML 示例、TypeScript 声明和公共 OpenAPI;外部发布前需使用真实 Google / Turnstile 配置完成联调。包名、npm 渠道、初始版本 0.1.0 和 MIT 许可已确定;实际发布状态以上方说明及 npm registry 为准。