openreport-sdk 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,117 @@
1
- # Temporary Holding Version
1
+ # openreport-sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ TypeScript and JavaScript clients for OpenReport. Node.js >= 20, with no runtime
4
+ dependencies. Keep application and bot keys server side.
5
+
6
+ ```sh
7
+ npm install openreport-sdk
8
+ ```
9
+
10
+ ## Application client
11
+
12
+ Third-party integrations use an application API key and the `/v1` routes.
13
+
14
+ ```ts
15
+ import { OpenReport, levelAtLeast } from 'openreport-sdk';
16
+
17
+ const api = new OpenReport(process.env.OPENREPORT_API_KEY!);
18
+ const status = await api.lookup('123456789012345678');
19
+ if (status.blacklisted && status.level && levelAtLeast(status.level, 'medium')) {
20
+ console.log(status.level, status.since);
21
+ }
22
+ console.log(status.staffRole); // 'jury', 'admin' or null, even if not listed.
23
+ const members = await api.lookupMany(['123456789012345678', '987654321098765432']);
24
+ console.log(members.get('123456789012345678'));
25
+
26
+ const receipt = await api.report({
27
+ userIds: ['123456789012345678'],
28
+ reason: 'Describe what happened with enough context for the jury.',
29
+ evidence: [{ name: 'capture.png', data: screenshotBytes, type: 'image/png' }],
30
+ });
31
+ console.log(receipt.reference);
32
+ ```
33
+
34
+ `screenshotBytes` is a `Uint8Array`, `ArrayBuffer` or `Blob` containing the evidence.
35
+ The API validates report contents and evidence. `lookupMany` deduplicates IDs,
36
+ requests batches of at most 100, and returns a `Map<string, Status>`. An empty
37
+ list makes no request. Lookup and report IDs must have 17 to 20 digits or the
38
+ SDK rejects with `TypeError`. Levels are `low`, `medium`, `high`, mildest first.
39
+ Staff roles identify active staff; they do not attest to NDA acceptance.
40
+
41
+ CommonJS consumers can use `const { OpenReport } = require('openreport-sdk')`.
42
+ The same compiled CommonJS package supports named ESM imports and includes types.
43
+
44
+ ## Official bot client
45
+
46
+ `OpenReportBot` is reserved to the official OpenReport bot. Third-party bots must
47
+ use `OpenReport`, and cannot act on behalf of users with an application key.
48
+
49
+ ```ts
50
+ import { OpenReportBot } from 'openreport-sdk';
51
+
52
+ const bot = new OpenReportBot(process.env.OPENREPORT_BOT_KEY!);
53
+ const actingUserId = '123456789012345678';
54
+ const cases = await bot.cases('open'); // Or 'recent'.
55
+ if (cases[0]) {
56
+ const detail = await bot.caseDetail(actingUserId, cases[0].id);
57
+ const tally = await bot.vote(actingUserId, detail.id, {
58
+ approve: true,
59
+ level: 'medium',
60
+ });
61
+ console.log(tally.quorumReached);
62
+ }
63
+ const record = await bot.record(actingUserId);
64
+ const status = await bot.lookup(actingUserId);
65
+ // bot.report(actingUserId, input) accepts the same ReportInput as api.report().
66
+ ```
67
+
68
+ The bot authenticates with `Authorization: Bot <key>` and sends the acting user
69
+ in `X-OpenReport-User` for report, detail, vote and record calls. The backend
70
+ checks juror/admin permissions and NDA acceptance for details and votes. Show
71
+ case reasons only to authorized jurors in ephemeral messages. Evidence must
72
+ never pass through Discord.
73
+
74
+ ## Options and errors
75
+
76
+ Both constructors accept `{ baseUrl, timeoutMs, fetch, userAgent }`. The default
77
+ base URL is `https://api-openreport.chell.fr`; the default timeout is 10 seconds.
78
+ Supply `fetch` to inject a compatible transport for tests. Requests use
79
+ `AbortSignal.timeout`; timeout errors retain the native `TimeoutError` name.
80
+ Network errors propagate, with no automatic retry of reports or votes.
81
+
82
+ Wire fields are snake_case; SDK fields are camelCase. Dates become `Date`
83
+ objects, including lookup timestamps supplied as Unix seconds. Absent lookup
84
+ role, level and date fields become `null` for compatibility with older servers.
85
+
86
+ API errors extend `OpenReportError` with `status` and `detail`:
87
+
88
+ | Error | Status | Extra |
89
+ | --- | --- | --- |
90
+ | `Unauthorized` | 401, 403 | Invalid key or acting user not authorized |
91
+ | `NotFound` | 404 | Unknown resource |
92
+ | `RateLimited` | 429 | `retryAfter`: seconds, or `null` if missing/invalid |
93
+ | `OpenReportError` | Other failures | Original error detail |
94
+
95
+ `Retry-After` accepts seconds or an HTTP date. Error detail retains structured
96
+ validation errors; plain text gateway responses are also preserved.
97
+
98
+ ## Notice obligation
99
+
100
+ GDPR article 14 requires your bot to inform a person when it uses registry data
101
+ to restrict them in a server, at the latest at their first interaction. Explain
102
+ the restriction and link to `https://openreport.chell.fr/notice` and their record
103
+ on the OpenReport site. Do not sanction someone for a pending accusation.
104
+
105
+ ## Development
106
+
107
+ ```sh
108
+ npm install
109
+ npm run build
110
+ npm test
111
+ npm pack --dry-run
112
+ ```
113
+
114
+ `build` compiles strict TypeScript to CommonJS and declarations in `dist/`.
115
+ `test` runs `node:test` through `tsx` with injected fake fetch implementations.
116
+ `prepublishOnly` runs the build and tests. Only `dist/`, this README and package
117
+ metadata are published. License: MIT.
@@ -0,0 +1,12 @@
1
+ import type { CaseDetail, CaseSummary, ClientOptions, ReportInput, ReportReceipt, SelfRecord, Status, Tally, VoteInput } from './types';
2
+ /** Reserved for the official OpenReport bot and its single bot key. */
3
+ export declare class OpenReportBot {
4
+ private readonly http;
5
+ constructor(botKey: string, options?: ClientOptions);
6
+ lookup(userId: string): Promise<Status>;
7
+ report(actingUserId: string, input: ReportInput): Promise<ReportReceipt>;
8
+ cases(state: 'open' | 'recent'): Promise<CaseSummary[]>;
9
+ caseDetail(actingUserId: string, caseId: string): Promise<CaseDetail>;
10
+ vote(actingUserId: string, caseId: string, vote: VoteInput): Promise<Tally>;
11
+ record(actingUserId: string): Promise<SelfRecord>;
12
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpenReportBot = void 0;
4
+ const http_1 = require("./http");
5
+ const mapping_1 = require("./mapping");
6
+ /** Reserved for the official OpenReport bot and its single bot key. */
7
+ class OpenReportBot {
8
+ http;
9
+ constructor(botKey, options) {
10
+ this.http = new http_1.Http(`Bot ${botKey}`, options);
11
+ }
12
+ async lookup(userId) {
13
+ (0, http_1.validateUserId)(userId);
14
+ return (0, mapping_1.status)(userId, await this.http.request(`/bot/lookup/${userId}`));
15
+ }
16
+ async report(actingUserId, input) {
17
+ return this.http.request('/bot/reports', 'POST', (0, http_1.reportForm)(input), actingUserId);
18
+ }
19
+ async cases(state) {
20
+ const cases = await this.http.request(`/bot/cases?state=${encodeURIComponent(state)}`);
21
+ return cases.map(mapping_1.caseSummary);
22
+ }
23
+ async caseDetail(actingUserId, caseId) {
24
+ return (0, mapping_1.caseDetail)(await this.http.request(`/bot/cases/${encodeURIComponent(caseId)}`, 'GET', undefined, actingUserId));
25
+ }
26
+ async vote(actingUserId, caseId, vote) {
27
+ return (0, mapping_1.tally)(await this.http.request(`/bot/cases/${encodeURIComponent(caseId)}/vote`, 'POST', {
28
+ approve: vote.approve,
29
+ danger_level: vote.level,
30
+ note: vote.note,
31
+ }, actingUserId));
32
+ }
33
+ async record(actingUserId) {
34
+ return (0, mapping_1.record)(await this.http.request('/bot/me/record', 'GET', undefined, actingUserId));
35
+ }
36
+ }
37
+ exports.OpenReportBot = OpenReportBot;
@@ -0,0 +1,9 @@
1
+ import type { ClientOptions, ReportInput, ReportReceipt, Status } from './types';
2
+ /** Application API key client for third-party integrations. */
3
+ export declare class OpenReport {
4
+ private readonly http;
5
+ constructor(apiKey: string, options?: ClientOptions);
6
+ lookup(userId: string): Promise<Status>;
7
+ lookupMany(userIds: string[]): Promise<Map<string, Status>>;
8
+ report(input: ReportInput): Promise<ReportReceipt>;
9
+ }
package/dist/client.js ADDED
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpenReport = void 0;
4
+ const http_1 = require("./http");
5
+ const mapping_1 = require("./mapping");
6
+ /** Application API key client for third-party integrations. */
7
+ class OpenReport {
8
+ http;
9
+ constructor(apiKey, options) {
10
+ this.http = new http_1.Http(`Bearer ${apiKey}`, options);
11
+ }
12
+ async lookup(userId) {
13
+ (0, http_1.validateUserId)(userId);
14
+ return (0, mapping_1.status)(userId, await this.http.request(`/v1/lookup/${userId}`));
15
+ }
16
+ async lookupMany(userIds) {
17
+ const ids = [...new Set(userIds)];
18
+ ids.forEach(http_1.validateUserId);
19
+ const result = new Map();
20
+ for (let start = 0; start < ids.length; start += 100) {
21
+ const chunk = ids.slice(start, start + 100);
22
+ const entries = await this.http.request('/v1/lookup', 'POST', { ids: chunk });
23
+ for (const userId of chunk) {
24
+ const entry = entries[userId];
25
+ if (entry === undefined) {
26
+ throw new TypeError(`Lookup response missing ${userId}`);
27
+ }
28
+ result.set(userId, (0, mapping_1.status)(userId, entry));
29
+ }
30
+ }
31
+ return result;
32
+ }
33
+ async report(input) {
34
+ return this.http.request('/v1/reports', 'POST', (0, http_1.reportForm)(input));
35
+ }
36
+ }
37
+ exports.OpenReport = OpenReport;
@@ -0,0 +1,14 @@
1
+ export declare class OpenReportError extends Error {
2
+ readonly status: number;
3
+ readonly detail: unknown;
4
+ constructor(status: number, detail: unknown);
5
+ }
6
+ export declare class Unauthorized extends OpenReportError {
7
+ }
8
+ export declare class NotFound extends OpenReportError {
9
+ }
10
+ export declare class RateLimited extends OpenReportError {
11
+ readonly retryAfter: number | null;
12
+ constructor(detail: unknown, retryAfter: number | null);
13
+ }
14
+ export declare function responseError(response: Response, body: unknown): OpenReportError;
package/dist/errors.js ADDED
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RateLimited = exports.NotFound = exports.Unauthorized = exports.OpenReportError = void 0;
4
+ exports.responseError = responseError;
5
+ class OpenReportError extends Error {
6
+ status;
7
+ detail;
8
+ constructor(status, detail) {
9
+ super(typeof detail === 'string' ? detail : `OpenReport request failed (${status})`);
10
+ this.status = status;
11
+ this.detail = detail;
12
+ this.name = new.target.name;
13
+ }
14
+ }
15
+ exports.OpenReportError = OpenReportError;
16
+ class Unauthorized extends OpenReportError {
17
+ }
18
+ exports.Unauthorized = Unauthorized;
19
+ class NotFound extends OpenReportError {
20
+ }
21
+ exports.NotFound = NotFound;
22
+ class RateLimited extends OpenReportError {
23
+ retryAfter;
24
+ constructor(detail, retryAfter) {
25
+ super(429, detail);
26
+ this.retryAfter = retryAfter;
27
+ }
28
+ }
29
+ exports.RateLimited = RateLimited;
30
+ function responseError(response, body) {
31
+ const detail = typeof body === 'object' && body !== null && 'detail' in body
32
+ ? body.detail : body;
33
+ if (response.status === 401 || response.status === 403) {
34
+ return new Unauthorized(response.status, detail);
35
+ }
36
+ if (response.status === 404) {
37
+ return new NotFound(response.status, detail);
38
+ }
39
+ if (response.status === 429) {
40
+ const header = response.headers.get('Retry-After');
41
+ let retryAfter = null;
42
+ if (header !== null && /^\d+(?:\.\d+)?$/.test(header.trim())) {
43
+ retryAfter = Number(header);
44
+ }
45
+ else if (header !== null) {
46
+ const date = Date.parse(header);
47
+ if (Number.isFinite(date)) {
48
+ retryAfter = Math.max(0, Math.ceil((date - Date.now()) / 1000));
49
+ }
50
+ }
51
+ return new RateLimited(detail, retryAfter);
52
+ }
53
+ return new OpenReportError(response.status, detail);
54
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { ClientOptions, ReportInput } from './types';
2
+ export declare function validateUserId(userId: string): void;
3
+ export declare function reportForm(input: ReportInput): FormData;
4
+ export declare class Http {
5
+ private readonly baseUrl;
6
+ private readonly timeoutMs;
7
+ private readonly fetcher;
8
+ private readonly headers;
9
+ constructor(authorization: string, options?: ClientOptions);
10
+ request<T>(path: string, method?: string, body?: FormData | object, actingUserId?: string): Promise<T>;
11
+ }
package/dist/http.js ADDED
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Http = void 0;
4
+ exports.validateUserId = validateUserId;
5
+ exports.reportForm = reportForm;
6
+ const errors_1 = require("./errors");
7
+ function validateUserId(userId) {
8
+ if (typeof userId !== 'string' || !/^\d{17,20}$/.test(userId)) {
9
+ throw new TypeError('Expected a Discord snowflake with 17 to 20 digits');
10
+ }
11
+ }
12
+ function reportForm(input) {
13
+ input.userIds.forEach(validateUserId);
14
+ const form = new FormData();
15
+ form.append('reason', input.reason);
16
+ for (const userId of input.userIds) {
17
+ form.append('discord_ids', userId);
18
+ }
19
+ for (const evidence of input.evidence) {
20
+ const data = evidence.data instanceof Uint8Array
21
+ ? new Uint8Array(evidence.data).buffer : evidence.data;
22
+ const blob = data instanceof Blob && evidence.type === undefined
23
+ ? data : new Blob([data], { type: evidence.type });
24
+ form.append('files', blob, evidence.name);
25
+ }
26
+ return form;
27
+ }
28
+ class Http {
29
+ baseUrl;
30
+ timeoutMs;
31
+ fetcher;
32
+ headers;
33
+ constructor(authorization, options = {}) {
34
+ this.baseUrl = (options.baseUrl ?? 'https://api-openreport.chell.fr').replace(/\/+$/, '');
35
+ this.timeoutMs = options.timeoutMs ?? 10_000;
36
+ this.fetcher = options.fetch ?? globalThis.fetch;
37
+ this.headers = new Headers({ Authorization: authorization });
38
+ if (options.userAgent !== undefined) {
39
+ this.headers.set('User-Agent', options.userAgent);
40
+ }
41
+ }
42
+ async request(path, method = 'GET', body, actingUserId) {
43
+ const headers = new Headers(this.headers);
44
+ if (actingUserId !== undefined) {
45
+ validateUserId(actingUserId);
46
+ headers.set('X-OpenReport-User', actingUserId);
47
+ }
48
+ const multipart = body instanceof FormData;
49
+ if (body !== undefined && !multipart) {
50
+ headers.set('Content-Type', 'application/json');
51
+ }
52
+ const response = await this.fetcher(`${this.baseUrl}${path}`, {
53
+ method,
54
+ headers,
55
+ body: multipart ? body : body === undefined ? undefined : JSON.stringify(body),
56
+ signal: AbortSignal.timeout(this.timeoutMs),
57
+ });
58
+ if (!response.ok) {
59
+ const text = await response.text();
60
+ let detail = text;
61
+ try {
62
+ detail = JSON.parse(text);
63
+ }
64
+ catch {
65
+ // Gateways can return plain text instead of the API's JSON errors.
66
+ }
67
+ throw (0, errors_1.responseError)(response, detail);
68
+ }
69
+ return await response.json();
70
+ }
71
+ }
72
+ exports.Http = Http;
@@ -0,0 +1,4 @@
1
+ export { OpenReport } from './client';
2
+ export { OpenReportBot } from './bot-client';
3
+ export { OpenReportError, Unauthorized, NotFound, RateLimited } from './errors';
4
+ export * from './types';
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.RateLimited = exports.NotFound = exports.Unauthorized = exports.OpenReportError = exports.OpenReportBot = exports.OpenReport = void 0;
18
+ var client_1 = require("./client");
19
+ Object.defineProperty(exports, "OpenReport", { enumerable: true, get: function () { return client_1.OpenReport; } });
20
+ var bot_client_1 = require("./bot-client");
21
+ Object.defineProperty(exports, "OpenReportBot", { enumerable: true, get: function () { return bot_client_1.OpenReportBot; } });
22
+ var errors_1 = require("./errors");
23
+ Object.defineProperty(exports, "OpenReportError", { enumerable: true, get: function () { return errors_1.OpenReportError; } });
24
+ Object.defineProperty(exports, "Unauthorized", { enumerable: true, get: function () { return errors_1.Unauthorized; } });
25
+ Object.defineProperty(exports, "NotFound", { enumerable: true, get: function () { return errors_1.NotFound; } });
26
+ Object.defineProperty(exports, "RateLimited", { enumerable: true, get: function () { return errors_1.RateLimited; } });
27
+ __exportStar(require("./types"), exports);
@@ -0,0 +1,51 @@
1
+ import type { CaseDetail, CaseSummary, SelfRecord, Status, Tally } from './types';
2
+ export interface WireStatus {
3
+ blacklisted: boolean;
4
+ blacklistlevel?: Status['level'];
5
+ blacklistdate?: number | null;
6
+ staff_role?: Status['staffRole'];
7
+ }
8
+ export declare function status(userId: string, wire: WireStatus): Status;
9
+ export interface WireTally {
10
+ jury_size: number;
11
+ cast: number;
12
+ approvals: number;
13
+ refusals: number;
14
+ quorum_needed: number;
15
+ quorum_reached: boolean;
16
+ approval_ratio: number;
17
+ admin_required: boolean;
18
+ }
19
+ export declare function tally(wire: WireTally): Tally;
20
+ export interface WireCase {
21
+ id: string;
22
+ reference: string;
23
+ kind: CaseSummary['kind'];
24
+ status: CaseSummary['status'];
25
+ opened_at: string;
26
+ expires_at: string | null;
27
+ subject_discord_id: string;
28
+ subject_display: string;
29
+ danger_level: CaseSummary['level'];
30
+ tally: WireTally;
31
+ }
32
+ export declare function caseSummary(wire: WireCase): CaseSummary;
33
+ export interface WireCaseDetail extends WireCase {
34
+ reason: string | null;
35
+ evidence_count: number;
36
+ my_vote: boolean | null;
37
+ }
38
+ export declare function caseDetail(wire: WireCaseDetail): CaseDetail;
39
+ export interface WireRecord {
40
+ discord_id: string;
41
+ blacklisted: boolean;
42
+ level: SelfRecord['level'];
43
+ blacklisted_at: string | null;
44
+ expires_at: string | null;
45
+ grounds_count: number;
46
+ open_cases: number;
47
+ can_appeal: boolean;
48
+ appeal_status: SelfRecord['appealStatus'];
49
+ informed_at: string | null;
50
+ }
51
+ export declare function record(wire: WireRecord): SelfRecord;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.status = status;
4
+ exports.tally = tally;
5
+ exports.caseSummary = caseSummary;
6
+ exports.caseDetail = caseDetail;
7
+ exports.record = record;
8
+ function status(userId, wire) {
9
+ return {
10
+ userId,
11
+ blacklisted: wire.blacklisted,
12
+ level: wire.blacklistlevel ?? null,
13
+ since: wire.blacklistdate == null ? null : new Date(wire.blacklistdate * 1000),
14
+ staffRole: wire.staff_role ?? null,
15
+ };
16
+ }
17
+ function tally(wire) {
18
+ return {
19
+ jurySize: wire.jury_size,
20
+ cast: wire.cast,
21
+ approvals: wire.approvals,
22
+ refusals: wire.refusals,
23
+ quorumNeeded: wire.quorum_needed,
24
+ quorumReached: wire.quorum_reached,
25
+ approvalRatio: wire.approval_ratio,
26
+ adminRequired: wire.admin_required,
27
+ };
28
+ }
29
+ function caseSummary(wire) {
30
+ return {
31
+ id: wire.id,
32
+ reference: wire.reference,
33
+ kind: wire.kind,
34
+ status: wire.status,
35
+ openedAt: new Date(wire.opened_at),
36
+ expiresAt: wire.expires_at === null ? null : new Date(wire.expires_at),
37
+ subjectId: wire.subject_discord_id,
38
+ subjectDisplay: wire.subject_display,
39
+ level: wire.danger_level,
40
+ tally: tally(wire.tally),
41
+ };
42
+ }
43
+ function caseDetail(wire) {
44
+ return {
45
+ ...caseSummary(wire),
46
+ reason: wire.reason,
47
+ evidenceCount: wire.evidence_count,
48
+ myVote: wire.my_vote,
49
+ };
50
+ }
51
+ function record(wire) {
52
+ return {
53
+ userId: wire.discord_id,
54
+ blacklisted: wire.blacklisted,
55
+ level: wire.level,
56
+ blacklistedAt: wire.blacklisted_at === null ? null : new Date(wire.blacklisted_at),
57
+ expiresAt: wire.expires_at === null ? null : new Date(wire.expires_at),
58
+ groundsCount: wire.grounds_count,
59
+ openCases: wire.open_cases,
60
+ canAppeal: wire.can_appeal,
61
+ appealStatus: wire.appeal_status,
62
+ informedAt: wire.informed_at === null ? null : new Date(wire.informed_at),
63
+ };
64
+ }
@@ -0,0 +1,78 @@
1
+ export type Level = 'low' | 'medium' | 'high';
2
+ export declare const LEVELS: readonly Level[];
3
+ export declare function levelAtLeast(level: Level, minimum: Level): boolean;
4
+ export type StaffRole = 'jury' | 'admin';
5
+ export interface Status {
6
+ userId: string;
7
+ blacklisted: boolean;
8
+ level: Level | null;
9
+ since: Date | null;
10
+ staffRole: StaffRole | null;
11
+ }
12
+ export interface Evidence {
13
+ name: string;
14
+ data: Uint8Array | ArrayBuffer | Blob;
15
+ type?: string;
16
+ }
17
+ export interface ReportInput {
18
+ userIds: string[];
19
+ reason: string;
20
+ evidence: Evidence[];
21
+ }
22
+ export interface ReportReceipt {
23
+ reference: string;
24
+ targets: number;
25
+ message: string;
26
+ }
27
+ export interface ClientOptions {
28
+ baseUrl?: string;
29
+ timeoutMs?: number;
30
+ fetch?: typeof fetch;
31
+ userAgent?: string;
32
+ }
33
+ export type CaseKind = 'blacklist' | 'unblacklist';
34
+ export type CaseStatus = 'open' | 'accepted' | 'rejected' | 'escalated' | 'expired' | 'withdrawn' | 'ignored';
35
+ export interface Tally {
36
+ jurySize: number;
37
+ cast: number;
38
+ approvals: number;
39
+ refusals: number;
40
+ quorumNeeded: number;
41
+ quorumReached: boolean;
42
+ approvalRatio: number;
43
+ adminRequired: boolean;
44
+ }
45
+ export interface CaseSummary {
46
+ id: string;
47
+ reference: string;
48
+ kind: CaseKind;
49
+ status: CaseStatus;
50
+ openedAt: Date;
51
+ expiresAt: Date | null;
52
+ subjectId: string;
53
+ subjectDisplay: string;
54
+ level: Level | null;
55
+ tally: Tally;
56
+ }
57
+ export interface CaseDetail extends CaseSummary {
58
+ reason: string | null;
59
+ evidenceCount: number;
60
+ myVote: boolean | null;
61
+ }
62
+ export interface VoteInput {
63
+ approve: boolean;
64
+ level?: Level;
65
+ note?: string;
66
+ }
67
+ export interface SelfRecord {
68
+ userId: string;
69
+ blacklisted: boolean;
70
+ level: Level | null;
71
+ blacklistedAt: Date | null;
72
+ expiresAt: Date | null;
73
+ groundsCount: number;
74
+ openCases: number;
75
+ canAppeal: boolean;
76
+ appealStatus: 'open' | 'accepted' | 'rejected' | null;
77
+ informedAt: Date | null;
78
+ }
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LEVELS = void 0;
4
+ exports.levelAtLeast = levelAtLeast;
5
+ exports.LEVELS = Object.freeze(['low', 'medium', 'high']);
6
+ function levelAtLeast(level, minimum) {
7
+ return exports.LEVELS.indexOf(level) >= exports.LEVELS.indexOf(minimum);
8
+ }
package/package.json CHANGED
@@ -1,6 +1,20 @@
1
1
  {
2
- "name": "openreport-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "openreport-sdk",
3
+ "version": "0.2.0",
4
+ "description": "OpenReport application and official bot clients",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": ["dist", "README.md"],
9
+ "engines": { "node": ">=20" },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "node --import tsx --test test/*.test.ts",
13
+ "prepublishOnly": "npm run build && npm test"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^22.0.0",
17
+ "tsx": "^4.0.0",
18
+ "typescript": "^5.0.0"
19
+ }
20
+ }