PKU harness
Concepts

Campus sessions and IAAA

The harness keeps the campus credentials, signs in to each PKU service itself, and gives plugins the responses through `services.campus.request`. Plugins never see passwords or session tokens.

PKU services are all signed in through IAAA (统一身份认证), the university's central login system. After that, each service keeps its session in its own way. Some use cookies, some need a single sign-on (SSO) hop through the portal, and some use a JWT.

The harness implements all of this sign-in logic once, inside the app, following the protocol reference pkucli. A bundled plugin doesn't deal with sign-in at all. It sends a request by service name, the harness sends it with the signed-in session attached, and the plugin receives the response. For example, to look up the campus-card balance:

const res = await ctx.services.campus.request('card', {
  url: '/berserker-app/ykt/tsm/queryCard?synAccessSource=h5',
});
// res: { status, url, headers, body }

Services the harness signs in to

The table lists the four main services and how the harness builds a session for each.

ServiceHostSession the harness builds
cardbdcard.pku.edu.cnIAAA → portal ssoLogin → redirectToCard → synjones JWT
itsits.pku.edu.cnITS web login (网络账号, the network account)
treeholetreehole.pku.edu.cn /chapi/…IAAA "PKU Helper" → JWT + device uuid
blackboardcourse.pku.edu.cn (教学网)IAAA → campusLogin cookie session

The harness also handles expired sessions. For card it refreshes the JWT on a 401 response. For treehole it signs in again on a 401. For blackboard it follows redirects automatically.

Adapters in the app (src/core/campus/adapters/) register more services: bdkj (room booking), cwfw (finance), cwpay (finance payments), elective (course selection), library (the library) and library-space (library space booking: discussion rooms and study seats).

CampusService is typed as 'card' | 'its' | 'treehole' | 'blackboard' | (string & {}). A request for a service that isn't registered is rejected. Declare the services you use in the plugin's scopes, so that users can see them on the plugin's page.

The request format

The second argument of request is a CampusRequest object.

type CampusRequest = {
  url: string;                       // absolute, or a path on the service's host
  method?: 'GET' | 'POST';
  headers?: Record<string, string>;
  body?: string;
  contentType?: string;
  responseType?: 'text' | 'base64';
  form?: Record<string, string>;     // url-encoded form, instead of body
  authIn?: 'headers' | 'form';       // put the session credential in the form too (card order API)
  sign?: (fields) => fields | Promise<fields>;  // add a signature over the final form fields
  followRedirects?: boolean;         // false = read `location` yourself
  multipart?: CampusMultipart;       // file uploads; bytes never pass through the plugin
  timeoutMs?: number;                // default 20 s, 120 s for multipart
};

Requests are sent by the app's native campus HTTP module, which works as follows.

  • It keeps a separate cookie jar for each session.
  • It lets you control redirects by hand.
  • It uses HTTP/1.1.
  • It includes the extra root certificates that some campus sites need.
  • It uses no proxy, so sites that only work on campus are called directly.

Every type is listed in SDK → campus.

Second factors

There are two kinds of second factor. The harness handles one kind, and plugins handle the other.

The harness handles IAAA's second factors: SMS, email and the mobile token (手机令牌). Any sign-in can show a prompt, including a session refresh in the background. The request then pauses until the user completes the check.

Checks added by a service itself are left to the plugin, because only the plugin knows that service's API. Treehole, for example, returns two codes that need a check:

  • 40002 means an SMS check is needed, using /chapi/api/jwt_send_msg and /chapi/api/jwt_msg_verify;
  • 40077 means a course-data token is needed.

The plugin turns these codes into a result such as { status: 'verification_required', methods: ['sms'], message }. The user then completes the check, or gives the code to the agent so that the agent can complete it. On Android, services.sms.awaitCode() offers one-tap reading of the SMS code.

If the user has bound the app's authenticator, services.campus.otp() returns the current mobile-token code, but never the token's secret. Use it for checks such as Treehole's course token.

Handing the user to a campus page

Sometimes a plugin needs the user to finish something in a browser. The most common case is paying for an order that the plugin created.

services.campus.openSignedIn(service) opens the service in the system browser, already signed in. It uses the service's own hand-off mechanism. The harness builds the URL, so the plugin never sees the token in it.

services.browser.open(url) opens any URL, such as a payment cashier page. If the URL is an app link such as weixin:// or alipays://, the system opens that app.

Rules

  • You must declare requires: ['campus-session']. If you reach sites that only work on campus, add 'campus-network' as well.
  • You must not ask the user for their campus password, whether in a screen, a tool input or a remote plugin. Remote plugins have no campus session today. Granting them limited sessions is on the roadmap.
  • Where pkucli already implements an endpoint or a quirk, you must port it and cite the source file in a comment. See Porting from pkucli.
  • You should check services.campus.status().signedIn first. If the user is not signed in, return a clear message such as "请先在设置里登录校园账号" (please sign in to your campus account in Settings) instead of a raw error.

On this page