Add separate user.ts file, covering the basic user API.
Publish Package / build-and-publish (push) Successful in 23s
Publish Package / build-and-publish (push) Successful in 23s
This commit is contained in:
1 parent
0dd11f7b3b
commit
add9450e35
2 files changed
+234
No files matched your search
@@ -13,6 +13,10 @@
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./user": {
|
||||
"types": "./dist/user.d.ts",
|
||||
"import": "./dist/user.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Standalone User API client for OwlBoard.
|
||||
*
|
||||
* Separate from the main v3 API client — the user API lives at
|
||||
* /api/user/v1 and returns plain JSON (no envelope wrapper).
|
||||
*
|
||||
* AI was used in the creation of the User API library file
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { UserClient } from '@owlboard/owlboard-ts/user';
|
||||
* ```
|
||||
*/
|
||||
|
||||
// ── Error types ─────────────────────────────────────────────
|
||||
|
||||
export type UserApiErrorCode =
|
||||
| 'VALIDATION'
|
||||
| 'AUTH'
|
||||
| 'NOT_FOUND'
|
||||
| 'RATE_LIMIT'
|
||||
| 'SERVER';
|
||||
|
||||
const ERROR_DESCRIPTIONS: Record<UserApiErrorCode, string> = {
|
||||
VALIDATION: 'The request was invalid or contained malformed data',
|
||||
AUTH: 'Authentication failed — missing or invalid API key',
|
||||
NOT_FOUND: 'The requested user account was not found',
|
||||
RATE_LIMIT: 'Rate limit exceeded — too many requests, try again shortly',
|
||||
SERVER: 'The server encountered an internal error',
|
||||
};
|
||||
|
||||
/**
|
||||
* Structured error thrown by {@link UserClient} methods.
|
||||
*
|
||||
* - `code` — machine-readable category for programmatic branching
|
||||
* - `status` — HTTP status code from the response
|
||||
* - `message` — human-readable description (safe to display as-is)
|
||||
* - `detail` — raw response body, if any, for debugging
|
||||
*/
|
||||
export class UserApiError extends Error {
|
||||
public readonly name = 'UserApiError';
|
||||
|
||||
constructor(
|
||||
public readonly code: UserApiErrorCode,
|
||||
public readonly status: number,
|
||||
message: string,
|
||||
public readonly detail?: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/** Build a {@link UserApiError} from a failed fetch {@link Response}. */
|
||||
static async fromResponse(res: Response): Promise<UserApiError> {
|
||||
const code = statusToCode(res.status);
|
||||
const detail = await res.text().catch(() => undefined);
|
||||
|
||||
// If the server sent a JSON body with a `message` or `msg` field, prefer it
|
||||
const serverMessage = parseJsonMessage(detail);
|
||||
|
||||
const message = serverMessage
|
||||
? `${ERROR_DESCRIPTIONS[code]}: ${serverMessage}`
|
||||
: ERROR_DESCRIPTIONS[code];
|
||||
|
||||
return new UserApiError(code, res.status, message, detail);
|
||||
}
|
||||
}
|
||||
|
||||
function statusToCode(status: number): UserApiErrorCode {
|
||||
switch (status) {
|
||||
case 400: return 'VALIDATION';
|
||||
case 401:
|
||||
case 403: return 'AUTH';
|
||||
case 404: return 'NOT_FOUND';
|
||||
case 429: return 'RATE_LIMIT';
|
||||
default: return 'SERVER';
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonMessage(body: string | undefined): string | undefined {
|
||||
if (!body) return undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(body) as Record<string, unknown>;
|
||||
const msg = parsed['message'] ?? parsed['msg'] ?? parsed['error'];
|
||||
return typeof msg === 'string' ? msg : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Response types ──────────────────────────────────────────
|
||||
|
||||
/** POST /users/registrations */
|
||||
export interface RegistrationResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** POST /users (verify code & create account) */
|
||||
export interface VerifyCodeResponse {
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
/** GET /users */
|
||||
export interface UserAccount {
|
||||
uuid: string;
|
||||
domain: string;
|
||||
atime: string;
|
||||
}
|
||||
|
||||
// ── Client ──────────────────────────────────────────────────
|
||||
|
||||
export class UserClient {
|
||||
private readonly baseUrl: string;
|
||||
|
||||
constructor(
|
||||
baseUrl: string,
|
||||
private readonly apiKey?: string,
|
||||
) {
|
||||
const root = baseUrl.replace(/\/+$/, '');
|
||||
this.baseUrl = root.endsWith('/api/user/v1')
|
||||
? root
|
||||
: `${root}/api/user/v1`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a registration verification code.
|
||||
*
|
||||
* `POST /users/registrations`
|
||||
*
|
||||
* No authentication required.
|
||||
*/
|
||||
async requestRegistration(
|
||||
email: string,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<RegistrationResponse> {
|
||||
const res = await fetcher(`${this.baseUrl}/users/registrations`, {
|
||||
method: 'POST',
|
||||
headers: this.headers(true),
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
if (!res.ok) throw await this.error(res);
|
||||
return (await res.json()) as RegistrationResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a registration code and create the user account.
|
||||
*
|
||||
* `POST /users`
|
||||
*
|
||||
* No authentication required. Returns the new API key.
|
||||
*/
|
||||
async verifyCode(
|
||||
code: string,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<VerifyCodeResponse> {
|
||||
const res = await fetcher(`${this.baseUrl}/users`, {
|
||||
method: 'POST',
|
||||
headers: this.headers(true),
|
||||
body: JSON.stringify({ code }),
|
||||
});
|
||||
|
||||
if (!res.ok) throw await this.error(res);
|
||||
return (await res.json()) as VerifyCodeResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the authenticated user exists.
|
||||
*
|
||||
* `HEAD /users`
|
||||
*
|
||||
* Requires `X-OWL-KEY`.
|
||||
* @returns `true` if 200, `false` if 404.
|
||||
*/
|
||||
async checkExists(fetcher: typeof fetch = fetch): Promise<boolean> {
|
||||
const res = await fetcher(`${this.baseUrl}/users`, {
|
||||
method: 'HEAD',
|
||||
headers: this.headers(),
|
||||
});
|
||||
|
||||
if (res.status === 200) return true;
|
||||
if (res.status === 404) return false;
|
||||
throw await this.error(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the authenticated user's account details.
|
||||
*
|
||||
* `GET /users`
|
||||
*
|
||||
* Requires `X-OWL-KEY`.
|
||||
*/
|
||||
async getAccount(fetcher: typeof fetch = fetch): Promise<UserAccount> {
|
||||
const res = await fetcher(`${this.baseUrl}/users`, {
|
||||
method: 'GET',
|
||||
headers: this.headers(),
|
||||
});
|
||||
|
||||
if (!res.ok) throw await this.error(res);
|
||||
return (await res.json()) as UserAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the authenticated user's account.
|
||||
*
|
||||
* `DELETE /users`
|
||||
*
|
||||
* Requires `X-OWL-KEY`.
|
||||
*/
|
||||
async deleteAccount(fetcher: typeof fetch = fetch): Promise<void> {
|
||||
const res = await fetcher(`${this.baseUrl}/users`, {
|
||||
method: 'DELETE',
|
||||
headers: this.headers(),
|
||||
});
|
||||
|
||||
if (!res.ok) throw await this.error(res);
|
||||
}
|
||||
|
||||
// ── Internals ───────────────────────────────────────────
|
||||
|
||||
private headers(hasBody: boolean = false): Record<string, string> {
|
||||
const h: Record<string, string> = {};
|
||||
if (hasBody) h['Content-Type'] = 'application/json';
|
||||
if (this.apiKey) h['X-OWL-KEY'] = this.apiKey;
|
||||
return h;
|
||||
}
|
||||
|
||||
private async error(res: Response): Promise<UserApiError> {
|
||||
return UserApiError.fromResponse(res);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user