# @sitepong/fastnode

A Node.js/TypeScript toolkit for iOS & Android build automation, code signing,
and App Store / TestFlight distribution. Inspired by Fastlane, rewritten in
TypeScript with no Ruby dependency.

## Install

```bash
npm install @sitepong/fastnode
```

## Quick start

### As a library

```ts
import { AppStoreConnectClient } from "@sitepong/fastnode/client";
import { createCertificate } from "@sitepong/fastnode/cert";

const client = new AppStoreConnectClient({
  keyId: process.env.APP_STORE_CONNECT_API_KEY_ID!,
  issuerId: process.env.APP_STORE_CONNECT_API_KEY_ISSUER_ID!,
  privateKey: process.env.APP_STORE_CONNECT_API_KEY!,
});

const cert = await createCertificate({
  client,
  type: "IOS_DISTRIBUTION",
  outputDir: "./certs",
});

console.log("Issued cert:", cert.certificate.id);
```

### CLI

```bash
npx @sitepong/fastnode --help
npx @sitepong/fastnode cert --development
npx @sitepong/fastnode sigh --bundle-id com.example.app
```

## Modules

Each module can be imported separately:

| Module | Purpose |
|---|---|
| `@sitepong/fastnode/client` | App Store Connect REST client (JWT) + Apple Developer Portal client (session) |
| `@sitepong/fastnode/cert` | Create, list, revoke signing certificates |
| `@sitepong/fastnode/sigh` | Create and download provisioning profiles |
| `@sitepong/fastnode/match` | Sync certs + profiles across machines via git/S3/local storage |
| `@sitepong/fastnode/produce` | Create App Store Connect apps and bundle IDs, manage capabilities |
| `@sitepong/fastnode/pilot` | Upload builds to TestFlight, manage beta groups, set changelog |
| `@sitepong/fastnode/deliver` | Upload metadata, screenshots, submit to App Review |
| `@sitepong/fastnode/pem` | Create APNS push notification certificates |
| `@sitepong/fastnode/gym` | Build and archive iOS apps via xcodebuild |
| `@sitepong/fastnode/scan` | Run tests via xcodebuild, parse xcresult |
| `@sitepong/fastnode/snapshot` | Capture localized screenshots across devices |
| `@sitepong/fastnode/trainer` | Parse `.xcresult` / `.plist` test output → JUnit XML |
| `@sitepong/fastnode/precheck` | Static-analyze App Store metadata against review rules |
| `@sitepong/fastnode/frameit` | Composite screenshots into device frames |

## Apple sign-in & one-click setup

`fastnode` ships a complete "sign in with your Apple Developer Apple ID
and provision everything" flow built on top of the same SIRP+2FA login
fastlane's `spaceship` uses. Given an email + password and a 2FA code,
it produces a fully-populated credential set without ever touching the
Apple Developer Portal UI.

### Resumable login (HTTP-friendly 2FA)

```ts
import {
  beginLogin,
  submitTwoFactorCode,
  requestSmsCode,
} from "@sitepong/fastnode/client";

// Step 1: SIRP. Returns either a logged-in session or a pending state
// describing the 2FA challenge.
const result = await beginLogin({ email, password });

if (result.kind === "logged_in") {
  // No 2FA on this account — go straight to provisioning
  console.log("Logged in:", result.session.userEmail);
} else {
  // Prompt the user for the 6-digit code Apple pushed to their device
  // (or call requestSmsCode() to fall back to SMS)
  const session = await submitTwoFactorCode({
    pending: result.pending,
    code: "123456",
    source: "device", // or "phone" if the user opted into SMS
  });
}
```

`beginLogin` and `submitTwoFactorCode` are split so the 2FA prompt can
be served by an HTTP request loop (e.g. a wizard in a web app), not
just an interactive CLI. The legacy `loginWithAppleId(...)` is still
available for one-shot CLI use.

### One-click orchestration

```ts
import {
  oneClickListTeams,
  oneClickInventory,
  oneClickSetup,
} from "@sitepong/fastnode";

// 1. List developer-portal teams (the alphanumeric teamIds, e.g. "W44274RVJ2")
const teams = await oneClickListTeams(session);

// 2. Inspect the team's current state — bundle ids, dist cert capacity,
// existing push certs, profiles. Used to pre-populate UI / detect the
// 3-distribution-cert limit before trying to provision.
const inventory = await oneClickInventory(session, teams[0].teamId);

// 3. Provision everything in one call
const result = await oneClickSetup({
  session,
  teamId: teams[0].teamId,
  bundleIdentifier: "com.example.myapp",
  appName: "My App",
  options: {
    generateApnsAuthKey: true,    // .p8 APNs auth key (modern push)
    generateAscApiKey: true,      // .p8 ASC API key (TestFlight uploads)
    enableSignInWithApple: true,  // bundle capability via REST API
    enablePushNotifications: true,
    revokeCertIds: [],            // dist cert ids to revoke first
  },
  onProgress: (event) => {
    // structured step events for live UI feedback
    if (event.type === "step_done") console.log("✓", event.label);
  },
});

console.log({
  bundleId: result.bundle_identifier,
  ascKeyId: result.asc_key_id,           // e.g. "MRA2DX6Y6W"
  ascIssuerId: result.asc_issuer_id,     // e.g. "cfadaf94-44ca-4c2c-..."
  ascP8: result.asc_api_key_base64,      // base64 .p8 PEM
  distP12: result.dist_cert_base64,      // base64 PKCS#12
  certPwd: result.cert_password,
  profile: result.provisioning_profile_base64,
  apnsKey: result.additional_certs.find(c => c.cert_type === "apns_key"),
});
```

Under the hood `oneClickSetup` runs:

1. Initialize the developer-portal CSRF context
2. (Optional) revoke the user's existing distribution cert(s) — Apple
   caps a team at 3 active distribution certs and 1 per user
3. Create or reuse the bundle id
4. Generate an RSA CSR + create a fresh distribution certificate, bundle
   it with the private key into a PKCS#12 (.p12) using `node-forge`
5. Create an App Store provisioning profile, picking a non-colliding
   date-stamped name (e.g. `My App App Store 2026-04-14`, `… (1)`, `… (2)`
   if the same day collides)
6. (Optional) create an APNs Authentication Key (.p8) via the developer
   portal's `account/auth/key/v2/create` endpoint and download the PEM
   immediately via `account/auth/key/download`
7. (Optional) create an App Store Connect API key (.p8) via the iris
   admin API at `iris/v1/apiKeys`, then download the .p8 + issuer id via
   the JSON-API sparse fieldset trick (`?fields[apiKeys]=privateKey&include=provider`)
   reverse-engineered from `@expo/apple-utils`
8. (Optional) flip Push Notifications and Sign in with Apple capabilities
   on the bundle id via the JWT-authenticated REST API (the legacy portal
   capability endpoint Apple removed)

Failures at any optional step are converted into structured warnings
rather than halting the orchestration, so the credential is always
saved with whatever could be obtained.

### `PortalClient` and `IrisClient`

Two lower-level clients are exported alongside the high-level orchestrator
for direct use:

```ts
import { PortalClient, IrisClient } from "@sitepong/fastnode/client";

// Apple Developer Portal — session-cookie auth, used by the legacy
// account/* endpoints (bundle ids, certs, profiles, devices, key
// management for service-tied auth keys like APNs / DeviceCheck).
const portal = new PortalClient(session, "W44274RVJ2");
await portal.init();
const apnsKey = await portal.createApnsAuthKey("My App APNs");
console.log(apnsKey.pem); // -----BEGIN PRIVATE KEY-----...

// App Store Connect iris admin API — same session cookies, used for
// the modern PUBLIC_API (App Store Connect) keys that show up under
// "Users and Access > Integrations > Keys".
const iris = new IrisClient(session);
const ascKey = await iris.createApiKey({
  nickname: "My App SitePong",
  roles: ["APP_MANAGER"],
  allAppsVisible: true,
  keyType: "PUBLIC_API",
});
const downloaded = await iris.fetchApiKeyPrivateKey(ascKey.id);
console.log(downloaded?.pem, downloaded?.issuerId);
```

Both clients deliberately stay close to the wire format — they do not
hide the JSON-API envelope or Apple's per-section CSRF quirks. The
high-level `oneClickSetup` is where the ergonomics live.

## Environment

The library pulls App Store Connect credentials from:

- `APP_STORE_CONNECT_API_KEY_ID` — the 10-character key ID
- `APP_STORE_CONNECT_API_KEY_ISSUER_ID` — the issuer UUID (omit for individual keys)
- `APP_STORE_CONNECT_API_KEY` — the `-----BEGIN PRIVATE KEY-----` PEM contents

## Requirements

- Node.js ≥ 20
- macOS for Xcode-driven operations (`gym`, `scan`, `snapshot`, `trainer`)
- `xcodebuild`, `security`, and `codesign` on PATH for code-signing flows

## License

MIT
