npm package: @nokinc-flur/sdk
Description: Flur Wallet SDK (sprint 1 scaffold)
Latest version: 0.1.7

# flur-sdk (Sprint 1 / Week 1)

Typed client scaffold aligned to backend OpenAPI.

## Install (local dev)
```bash
npm ci
npm test
npm run build
```

## Generate types (optional)
This repo includes a minimal OpenAPI file at `openapi/flur.openapi.json`.

```bash
npm run gen
```

## E2E test against a running backend (optional)
1) Start backend locally (see backend README)
2) Run (E2E executes when RUN_E2E=1 or FLUR_BASE_URL is set):
```bash
FLUR_BASE_URL=http://localhost:8080 RUN_E2E=1 npm test
```

Contract-focused E2E only:
```bash
FLUR_BASE_URL=https://your-oci-gateway.example.com RUN_E2E=1 npm run test:contract
```

Optional envs for full send-money E2E coverage:
- `FLUR_E2E_SESSION_TOKEN` (required for account/transactions/recipient)
- `FLUR_E2E_RECIPIENT_IDENTIFIER` (required for recipient resolve / transfer)
- `FLUR_E2E_SEND_AUTH_TOKEN` (required for transfer create)
- `FLUR_E2E_DEVICE_ID` (required for transfer create)
- `FLUR_E2E_AMOUNT_MINOR` (optional, default `100`)
- `FLUR_E2E_CURRENCY` (optional, default `NGN`)

Generate real E2E auth inputs against deployed backend:
```bash
# Step 1: start onboarding and get requestId
npm run e2e:bootstrap -- --base-url https://your-oci-gateway --phone +23480xxxxxxx

# Step 2: rerun with OTP/silent-auth code (and optional recipient)
npm run e2e:bootstrap -- --base-url https://your-oci-gateway --phone +23480xxxxxxx --code 123456 --recipient +23481xxxxxxx

# Script prints PowerShell env exports, then run:
npm run test:contract
```

## Onboarding flow methods

```ts
import { FlurClient } from "@nokinc-flur/sdk";

const client = new FlurClient({ baseUrl: "https://api.example.com" });

const started = await client.onboardingStart({
	phoneE164: "+14155550123",
	appInstanceId: "app-instance-1",
	platform: "ios",
});
// started: { requestId, checkUrl?, expiresInSec, fallback }

const completed = await client.onboardingComplete({
	requestId: started.requestId,
	code: "123456",
	appInstanceId: "app-instance-1",
});
// completed: { sessionToken, userId, restricted, risk_reasons }
```

## Auth + device security methods

```ts
const registered = await client.registerDevice({
	userId: completed.userId,
	appInstanceId: "app-instance-1",
	platform: "ios",
	networkSignals: { ip: "1.2.3.4" },
}, { accessToken: completed.sessionToken });
// { deviceId, fingerprintHash, driftScore, trustState, stepUpRequired }

const refreshed = await client.authRefresh({
	userId: completed.userId,
	refreshToken: completed.sessionToken,
	appInstanceId: "app-instance-1",
	fingerprintHash: registered.fingerprintHash,
});
// { refreshToken, stepUpRequired }

await client.pinSet({ userId: completed.userId, pin: "123456" }, { accessToken: completed.sessionToken });
await client.pinVerify({ userId: completed.userId, pin: "123456" }, { accessToken: completed.sessionToken });
await client.authLogout(
	{ userId: completed.userId, refreshToken: refreshed.refreshToken },
	{ accessToken: refreshed.refreshToken }
);
```

## Send-money methods

```ts
// Register per-device signing key for send authorization
await client.registerSendDeviceKey({
	userId: completed.userId,
	deviceId: registered.deviceId,
	publicKey: "-----BEGIN PUBLIC KEY-----...",
}, { accessToken: completed.sessionToken });

// Obtain challenge and verify signature to get short-lived send token
const challenge = await client.createSendChallenge({
	userId: completed.userId,
	deviceId: registered.deviceId,
}, { accessToken: completed.sessionToken });

const send = await client.verifySendChallenge({
	userId: completed.userId,
	deviceId: registered.deviceId,
	challengeId: challenge.challengeId,
	signature: "base64-signature",
}, { accessToken: completed.sessionToken });

// Resolve recipient and create transfer
const recipient = await client.resolveRecipient(
	{ identifier: "+14155550123" },
	{ accessToken: completed.sessionToken }
);

await client.createTransfer(
	{
		recipientIdentifier: recipient.normalizedIdentifier,
		amountMinor: 5000,
		currency: "NGN",
		sendAuthToken: send.sendAuthToken,
	},
	{
		accessToken: completed.sessionToken,
		deviceId: registered.deviceId,
		idempotencyKey: crypto.randomUUID(),
	}
);

// Account + transaction endpoints
await client.accountSummary({ accessToken: completed.sessionToken });
await client.listTransactions({ accessToken: completed.sessionToken, limit: 20 });
```

## Error code mapping

SDK maps backend error payload `code` into typed `FlurError.code` when available:
- `TOKEN_REPLAYED`
- `SESSION_MISMATCH`
- `PIN_INVALID`
- `PIN_LOCKED`
- `PIN_NOT_SET`
- `STEP_UP_REQUIRED`

Fallback codes remain:
- `HTTP_ERROR`, `NETWORK_ERROR`, `TIMEOUT`, `UNKNOWN`

## Lean prod release workflow (one-person team)

PowerShell script (Windows):

```powershell
# patch release (recommended for this onboarding addition)
./scripts/release.ps1 -Bump patch

# exact version release (example: 1.0.1)
./scripts/release.ps1 -Version 1.0.1

# include E2E run (uses default OCI API Gateway URL from release script)
./scripts/release.ps1 -Version 1.0.1 -RunE2E

# include E2E run with override URL
./scripts/release.ps1 -Version 1.0.1 -RunE2E -E2EBaseUrl https://your-api.example.com

# publish to npm after all checks pass
./scripts/release.ps1 -Bump patch -Publish
./scripts/release.ps1 -Version 1.0.1 -Publish
```

What it does:
- verifies git working tree is clean
- runs `npm ci`
- runs `npm run lint`, `npm run typecheck`, `npm test`, `npm run build`
- creates tarball via `npm pack` and smoke-tests install in a temp project
- bumps version (`patch`/`minor`/`major`) and creates git tag
- pushes commit and tag (`git push`, `git push --tags`)
- optionally publishes to npm (`-Publish`)

NPM aliases:

```bash
npm run release:patch
npm run release:minor
npm run release:major
```