@rallycry/conveyor-incidents 0.1.0 → 0.3.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,6 +1,6 @@
1
1
  # @rallycry/conveyor-incidents
2
2
 
3
- Typed TypeScript client for reporting incidents to [Conveyor](https://conveyor.rallycry.gg) via the public REST API.
3
+ Typed TypeScript client for reporting incidents to [Conveyor](https://conveyor.rallycryapp.com) via the public REST API.
4
4
 
5
5
  ## Install
6
6
 
@@ -36,6 +36,61 @@ const { incidentId, attachmentResults } = await incidents.report({
36
36
  });
37
37
  ```
38
38
 
39
+ ### Card types
40
+
41
+ `report()` accepts an optional `type` — `"incident"` (default), `"suggestion"`, or `"task"`:
42
+
43
+ - `"incident"` — fingerprint dedup (see below); repeat reports collapse into one incident.
44
+ - `"suggestion"` — AI duplicate detection; a repeat report upvotes the existing suggestion and returns its id instead of creating a new card. `fingerprintKey` and `severity` are ignored (suggestions are ranked by votes).
45
+ - `"task"` — creates a plain task card. No dedup; `fingerprintKey` is ignored. `severity` maps to the card's priority.
46
+
47
+ ```ts
48
+ // Platform-user suggestion, routed to the team for review
49
+ await incidents.report({
50
+ type: "suggestion",
51
+ title: "Add dark mode toggle",
52
+ assigned: ["logan@rallycry.gg"],
53
+ review: ["dev1@rallycry.gg", "dev2@rallycry.gg"],
54
+ });
55
+ ```
56
+
57
+ ### Assigning members by email
58
+
59
+ `assigned` and `review` are optional arrays of email addresses, matched against the target project's members:
60
+
61
+ - `assigned` — the **first** email that resolves to a project member becomes the card's assignee; remaining entries are ignored.
62
+ - `review` — **every** email that resolves to a project member is added as a reviewer.
63
+ - Emails that don't match a project member are **silently skipped** — safe to pass end-user emails; only teammates who are members of the Conveyor project resolve.
64
+ - Applied **only when a new card is created**. Dedup hits (incident fingerprint match, suggestion AI-merge) leave the existing card untouched.
65
+
66
+ ```ts
67
+ // Incident auto-assigned when reporter is @rallycry.gg
68
+ await incidents.report({
69
+ title: "Checkout service returned 500",
70
+ severity: "high",
71
+ fingerprintKey: "checkout-500-v1.4.2",
72
+ assigned: ["logan@rallycry.gg"],
73
+ });
74
+ ```
75
+
76
+ ### Creator attribution (`createdBy`)
77
+
78
+ `createdBy` is an optional single email, matched against the target project's members the same way as `assigned`/`review`:
79
+
80
+ - A resolvable project-member email becomes the card's creator.
81
+ - When omitted — or when the email doesn't resolve to a member — the card is attributed to the **"Conveyor Incidents" system user**, not a human.
82
+ - Applied **only when a new card is created**; dedup hits keep the existing creator.
83
+
84
+ > **Behavior change in 0.3.0:** previously, cards reported through this endpoint were attributed to the token owner (suggestions/tasks) or the project owner (incidents). Pass `createdBy` explicitly if you want a specific member credited.
85
+
86
+ ```ts
87
+ await incidents.report({
88
+ type: "task",
89
+ title: "Wire up the new webhook",
90
+ createdBy: "logan@rallycry.gg",
91
+ });
92
+ ```
93
+
39
94
  ### About `source` and `fingerprintKey`
40
95
 
41
96
  Conveyor deduplicates reports by a fingerprint derived from `(projectId, source, fingerprintKey)` — or by `(projectId, source, title)` if `fingerprintKey` is omitted. **Use stable values** for `source` (e.g. `"prod-web"`, not `"node-pid-1234"`) so that repeat reports collapse into a single incident instead of creating noise.
@@ -49,13 +104,21 @@ Conveyor deduplicates reports by a fingerprint derived from `(projectId, source,
49
104
 
50
105
  Pass a `fetch` polyfill via `opts.fetch` if your runtime lacks a global `fetch`.
51
106
 
107
+ ### `DEP0169 url.parse()` warning
108
+
109
+ The `@rallycry/conveyor-incidents` SDK does not call `url.parse()` — it uses the WHATWG `URL` constructor and the global `fetch` API. If your process emits this warning while using the SDK, the call is coming from another dependency in your project. Run `node --trace-deprecation your-script.js` to locate the source.
110
+
111
+ ### Troubleshooting `403` from `/api/incidents/report`
112
+
113
+ The endpoint requires a project token with `scope: "incident"`. After the 2026-04-22 scope migration, older project tokens default to `scope: "claude_code"` and are rejected. The error body now includes a `reason` field — `"scope_mismatch"` indicates the token exists but is scoped for another surface; generate a new token from **Project Settings → Incidents** in the Conveyor UI.
114
+
52
115
  ## API
53
116
 
54
117
  ```ts
55
118
  new ConveyorIncidents({
56
119
  apiKey: string;
57
120
  projectId: string;
58
- baseUrl?: string; // default: "https://conveyor.rallycry.gg"
121
+ baseUrl?: string; // default: "https://api.conveyor.rallycryapp.com"
59
122
  fetch?: typeof fetch;
60
123
  });
61
124
 
@@ -66,6 +129,10 @@ client.report({
66
129
  source?: string; // default: "client"
67
130
  fingerprintKey?: string;
68
131
  plan?: string;
132
+ type?: "incident" | "suggestion" | "task"; // default: "incident"
133
+ assigned?: string[]; // project-member emails; first match becomes assignee
134
+ review?: string[]; // project-member emails; every match becomes a reviewer
135
+ createdBy?: string; // project-member email; no match → "Conveyor Incidents" system user
69
136
  attachments?: Array<{
70
137
  fileName: string;
71
138
  mimeType: string;
@@ -73,6 +140,7 @@ client.report({
73
140
  }>;
74
141
  }): Promise<{
75
142
  incidentId: string;
143
+ cardId: string; // alias of incidentId — prefer for suggestion/task types
76
144
  attachmentResults: Array<{
77
145
  fileName: string;
78
146
  ok: boolean;
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ declare class ConveyorIncidentsError extends Error {
5
5
  }
6
6
 
7
7
  type IncidentSeverity = "critical" | "high" | "medium" | "low";
8
+ type ConveyorCardType = "incident" | "suggestion" | "task";
8
9
  interface ConveyorIncidentsOptions {
9
10
  apiKey: string;
10
11
  projectId: string;
@@ -24,6 +25,26 @@ interface ReportIncidentInput {
24
25
  fingerprintKey?: string;
25
26
  plan?: string;
26
27
  attachments?: IncidentAttachment[];
28
+ /** Card type to create. The server defaults to "incident" when omitted. */
29
+ type?: ConveyorCardType;
30
+ /**
31
+ * Project-member emails. The FIRST email that resolves to a member of the
32
+ * target project becomes the card's assignee; the rest are ignored.
33
+ * Non-member emails are silently skipped.
34
+ */
35
+ assigned?: string[];
36
+ /**
37
+ * Project-member emails. EVERY email that resolves to a member of the
38
+ * target project is added as a reviewer. Non-member emails are silently
39
+ * skipped.
40
+ */
41
+ review?: string[];
42
+ /**
43
+ * Project-member email used as the card's creator. When omitted — or when
44
+ * the email doesn't resolve to a member of the target project — the card
45
+ * is attributed to the "Conveyor Incidents" system user.
46
+ */
47
+ createdBy?: string;
27
48
  }
28
49
  interface AttachmentResult {
29
50
  fileName: string;
@@ -33,6 +54,8 @@ interface AttachmentResult {
33
54
  }
34
55
  interface ReportIncidentResult {
35
56
  incidentId: string;
57
+ /** Alias of `incidentId` — prefer this when reporting type "suggestion" or "task". */
58
+ cardId: string;
36
59
  attachmentResults: AttachmentResult[];
37
60
  }
38
61
  declare class ConveyorIncidents {
@@ -45,4 +68,4 @@ declare class ConveyorIncidents {
45
68
  private uploadAttachment;
46
69
  }
47
70
 
48
- export { type AttachmentResult, ConveyorIncidents, ConveyorIncidentsError, type ConveyorIncidentsOptions, type IncidentAttachment, type IncidentSeverity, type ReportIncidentInput, type ReportIncidentResult };
71
+ export { type AttachmentResult, type ConveyorCardType, ConveyorIncidents, ConveyorIncidentsError, type ConveyorIncidentsOptions, type IncidentAttachment, type IncidentSeverity, type ReportIncidentInput, type ReportIncidentResult };
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ var ConveyorIncidentsError = class extends Error {
11
11
  };
12
12
 
13
13
  // src/index.ts
14
- var DEFAULT_BASE_URL = "https://conveyor.rallycry.gg";
14
+ var DEFAULT_BASE_URL = "https://api.conveyor.rallycryapp.com";
15
15
  var DEFAULT_SOURCE = "client";
16
16
  var DEFAULT_SEVERITY = "medium";
17
17
  var ConveyorIncidents = class {
@@ -43,7 +43,11 @@ var ConveyorIncidents = class {
43
43
  title: input.title,
44
44
  description: input.description,
45
45
  plan: input.plan,
46
- fingerprintKey: input.fingerprintKey
46
+ fingerprintKey: input.fingerprintKey,
47
+ type: input.type,
48
+ assigned: input.assigned,
49
+ review: input.review,
50
+ createdBy: input.createdBy
47
51
  };
48
52
  const res = await this.fetchImpl(`${this.baseUrl}/api/incidents/report`, {
49
53
  method: "POST",
@@ -61,7 +65,7 @@ var ConveyorIncidents = class {
61
65
  payload
62
66
  );
63
67
  }
64
- const incidentId = payload && typeof payload === "object" && "incidentId" in payload ? String(payload.incidentId) : void 0;
68
+ const incidentId = extractId(payload, "incidentId") ?? extractId(payload, "cardId");
65
69
  if (!incidentId) {
66
70
  throw new ConveyorIncidentsError(
67
71
  "Invalid response from /api/incidents/report: missing incidentId",
@@ -75,7 +79,7 @@ var ConveyorIncidents = class {
75
79
  attachmentResults.push(await this.uploadAttachment(incidentId, attachment));
76
80
  }
77
81
  }
78
- return { incidentId, attachmentResults };
82
+ return { incidentId, cardId: incidentId, attachmentResults };
79
83
  }
80
84
  async uploadAttachment(incidentId, attachment) {
81
85
  try {
@@ -117,6 +121,13 @@ var ConveyorIncidents = class {
117
121
  function stripTrailingSlash(url) {
118
122
  return url.endsWith("/") ? url.slice(0, -1) : url;
119
123
  }
124
+ function extractId(payload, key) {
125
+ if (payload && typeof payload === "object" && key in payload) {
126
+ const value = payload[key];
127
+ if (value !== void 0 && value !== null) return String(value);
128
+ }
129
+ return void 0;
130
+ }
120
131
  async function readJson(res) {
121
132
  const text = await res.text();
122
133
  if (!text) return void 0;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/index.ts"],"sourcesContent":["export class ConveyorIncidentsError extends Error {\n readonly status: number;\n readonly body: unknown;\n\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.name = \"ConveyorIncidentsError\";\n this.status = status;\n this.body = body;\n }\n}\n","import { ConveyorIncidentsError } from \"./errors.js\";\n\nexport { ConveyorIncidentsError } from \"./errors.js\";\n\nconst DEFAULT_BASE_URL = \"https://conveyor.rallycry.gg\";\nconst DEFAULT_SOURCE = \"client\";\nconst DEFAULT_SEVERITY: IncidentSeverity = \"medium\";\n\nexport type IncidentSeverity = \"critical\" | \"high\" | \"medium\" | \"low\";\n\nexport interface ConveyorIncidentsOptions {\n apiKey: string;\n projectId: string;\n baseUrl?: string;\n fetch?: typeof fetch;\n}\n\nexport interface IncidentAttachment {\n fileName: string;\n mimeType: string;\n content: Uint8Array | string;\n}\n\nexport interface ReportIncidentInput {\n title: string;\n description?: string;\n severity?: IncidentSeverity;\n source?: string;\n fingerprintKey?: string;\n plan?: string;\n attachments?: IncidentAttachment[];\n}\n\nexport interface AttachmentResult {\n fileName: string;\n ok: boolean;\n fileId?: string;\n error?: string;\n}\n\nexport interface ReportIncidentResult {\n incidentId: string;\n attachmentResults: AttachmentResult[];\n}\n\nexport class ConveyorIncidents {\n private readonly apiKey: string;\n private readonly projectId: string;\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: ConveyorIncidentsOptions) {\n if (!opts.apiKey) {\n throw new Error(\"ConveyorIncidents: apiKey is required\");\n }\n if (!opts.projectId) {\n throw new Error(\"ConveyorIncidents: projectId is required\");\n }\n const resolvedFetch = opts.fetch ?? globalThis.fetch;\n if (typeof resolvedFetch !== \"function\") {\n throw new Error(\"ConveyorIncidents: no global fetch available — pass opts.fetch on Node <18\");\n }\n this.apiKey = opts.apiKey;\n this.projectId = opts.projectId;\n this.baseUrl = stripTrailingSlash(opts.baseUrl ?? DEFAULT_BASE_URL);\n this.fetchImpl = resolvedFetch;\n }\n\n async report(input: ReportIncidentInput): Promise<ReportIncidentResult> {\n const body = {\n projectId: this.projectId,\n source: input.source ?? DEFAULT_SOURCE,\n severity: input.severity ?? DEFAULT_SEVERITY,\n title: input.title,\n description: input.description,\n plan: input.plan,\n fingerprintKey: input.fingerprintKey,\n };\n\n const res = await this.fetchImpl(`${this.baseUrl}/api/incidents/report`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(body),\n });\n\n const payload = await readJson(res);\n\n if (!res.ok) {\n throw new ConveyorIncidentsError(\n `Failed to report incident: ${res.status} ${res.statusText}`,\n res.status,\n payload,\n );\n }\n\n const incidentId =\n payload && typeof payload === \"object\" && \"incidentId\" in payload\n ? String((payload as { incidentId: unknown }).incidentId)\n : undefined;\n\n if (!incidentId) {\n throw new ConveyorIncidentsError(\n \"Invalid response from /api/incidents/report: missing incidentId\",\n res.status,\n payload,\n );\n }\n\n const attachmentResults: AttachmentResult[] = [];\n if (input.attachments && input.attachments.length > 0) {\n for (const attachment of input.attachments) {\n attachmentResults.push(await this.uploadAttachment(incidentId, attachment));\n }\n }\n\n return { incidentId, attachmentResults };\n }\n\n private async uploadAttachment(\n incidentId: string,\n attachment: IncidentAttachment,\n ): Promise<AttachmentResult> {\n try {\n const contentBase64 = toBase64(attachment.content);\n const res = await this.fetchImpl(\n `${this.baseUrl}/api/incidents/${encodeURIComponent(incidentId)}/files`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({\n fileName: attachment.fileName,\n mimeType: attachment.mimeType,\n contentBase64,\n }),\n },\n );\n\n const payload = await readJson(res);\n\n if (!res.ok) {\n return {\n fileName: attachment.fileName,\n ok: false,\n error: formatErrorBody(res.status, res.statusText, payload),\n };\n }\n\n const fileId =\n payload && typeof payload === \"object\" && \"fileId\" in payload\n ? String((payload as { fileId: unknown }).fileId)\n : undefined;\n\n return { fileName: attachment.fileName, ok: true, fileId };\n } catch (err) {\n return {\n fileName: attachment.fileName,\n ok: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.endsWith(\"/\") ? url.slice(0, -1) : url;\n}\n\nasync function readJson(res: Response): Promise<unknown> {\n const text = await res.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction formatErrorBody(status: number, statusText: string, body: unknown): string {\n const prefix = `${status} ${statusText}`.trim();\n if (body && typeof body === \"object\" && \"error\" in body) {\n return `${prefix}: ${String((body as { error: unknown }).error)}`;\n }\n if (typeof body === \"string\" && body.length > 0) {\n return `${prefix}: ${body}`;\n }\n return prefix;\n}\n\nfunction toBase64(content: Uint8Array | string): string {\n const bytes = typeof content === \"string\" ? new TextEncoder().encode(content) : content;\n return bytesToBase64(bytes);\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(bytes).toString(\"base64\");\n }\n let binary = \"\";\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return btoa(binary);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/index.ts"],"sourcesContent":["export class ConveyorIncidentsError extends Error {\n readonly status: number;\n readonly body: unknown;\n\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.name = \"ConveyorIncidentsError\";\n this.status = status;\n this.body = body;\n }\n}\n","import { ConveyorIncidentsError } from \"./errors.js\";\n\nexport { ConveyorIncidentsError } from \"./errors.js\";\n\nconst DEFAULT_BASE_URL = \"https://api.conveyor.rallycryapp.com\";\nconst DEFAULT_SOURCE = \"client\";\nconst DEFAULT_SEVERITY: IncidentSeverity = \"medium\";\n\nexport type IncidentSeverity = \"critical\" | \"high\" | \"medium\" | \"low\";\n\nexport type ConveyorCardType = \"incident\" | \"suggestion\" | \"task\";\n\nexport interface ConveyorIncidentsOptions {\n apiKey: string;\n projectId: string;\n baseUrl?: string;\n fetch?: typeof fetch;\n}\n\nexport interface IncidentAttachment {\n fileName: string;\n mimeType: string;\n content: Uint8Array | string;\n}\n\nexport interface ReportIncidentInput {\n title: string;\n description?: string;\n severity?: IncidentSeverity;\n source?: string;\n fingerprintKey?: string;\n plan?: string;\n attachments?: IncidentAttachment[];\n /** Card type to create. The server defaults to \"incident\" when omitted. */\n type?: ConveyorCardType;\n /**\n * Project-member emails. The FIRST email that resolves to a member of the\n * target project becomes the card's assignee; the rest are ignored.\n * Non-member emails are silently skipped.\n */\n assigned?: string[];\n /**\n * Project-member emails. EVERY email that resolves to a member of the\n * target project is added as a reviewer. Non-member emails are silently\n * skipped.\n */\n review?: string[];\n /**\n * Project-member email used as the card's creator. When omitted — or when\n * the email doesn't resolve to a member of the target project — the card\n * is attributed to the \"Conveyor Incidents\" system user.\n */\n createdBy?: string;\n}\n\nexport interface AttachmentResult {\n fileName: string;\n ok: boolean;\n fileId?: string;\n error?: string;\n}\n\nexport interface ReportIncidentResult {\n incidentId: string;\n /** Alias of `incidentId` — prefer this when reporting type \"suggestion\" or \"task\". */\n cardId: string;\n attachmentResults: AttachmentResult[];\n}\n\nexport class ConveyorIncidents {\n private readonly apiKey: string;\n private readonly projectId: string;\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: ConveyorIncidentsOptions) {\n if (!opts.apiKey) {\n throw new Error(\"ConveyorIncidents: apiKey is required\");\n }\n if (!opts.projectId) {\n throw new Error(\"ConveyorIncidents: projectId is required\");\n }\n const resolvedFetch = opts.fetch ?? globalThis.fetch;\n if (typeof resolvedFetch !== \"function\") {\n throw new Error(\"ConveyorIncidents: no global fetch available — pass opts.fetch on Node <18\");\n }\n this.apiKey = opts.apiKey;\n this.projectId = opts.projectId;\n this.baseUrl = stripTrailingSlash(opts.baseUrl ?? DEFAULT_BASE_URL);\n this.fetchImpl = resolvedFetch;\n }\n\n async report(input: ReportIncidentInput): Promise<ReportIncidentResult> {\n const body = {\n projectId: this.projectId,\n source: input.source ?? DEFAULT_SOURCE,\n severity: input.severity ?? DEFAULT_SEVERITY,\n title: input.title,\n description: input.description,\n plan: input.plan,\n fingerprintKey: input.fingerprintKey,\n type: input.type,\n assigned: input.assigned,\n review: input.review,\n createdBy: input.createdBy,\n };\n\n const res = await this.fetchImpl(`${this.baseUrl}/api/incidents/report`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(body),\n });\n\n const payload = await readJson(res);\n\n if (!res.ok) {\n throw new ConveyorIncidentsError(\n `Failed to report incident: ${res.status} ${res.statusText}`,\n res.status,\n payload,\n );\n }\n\n const incidentId = extractId(payload, \"incidentId\") ?? extractId(payload, \"cardId\");\n\n if (!incidentId) {\n throw new ConveyorIncidentsError(\n \"Invalid response from /api/incidents/report: missing incidentId\",\n res.status,\n payload,\n );\n }\n\n const attachmentResults: AttachmentResult[] = [];\n if (input.attachments && input.attachments.length > 0) {\n for (const attachment of input.attachments) {\n attachmentResults.push(await this.uploadAttachment(incidentId, attachment));\n }\n }\n\n return { incidentId, cardId: incidentId, attachmentResults };\n }\n\n private async uploadAttachment(\n incidentId: string,\n attachment: IncidentAttachment,\n ): Promise<AttachmentResult> {\n try {\n const contentBase64 = toBase64(attachment.content);\n const res = await this.fetchImpl(\n `${this.baseUrl}/api/incidents/${encodeURIComponent(incidentId)}/files`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({\n fileName: attachment.fileName,\n mimeType: attachment.mimeType,\n contentBase64,\n }),\n },\n );\n\n const payload = await readJson(res);\n\n if (!res.ok) {\n return {\n fileName: attachment.fileName,\n ok: false,\n error: formatErrorBody(res.status, res.statusText, payload),\n };\n }\n\n const fileId =\n payload && typeof payload === \"object\" && \"fileId\" in payload\n ? String((payload as { fileId: unknown }).fileId)\n : undefined;\n\n return { fileName: attachment.fileName, ok: true, fileId };\n } catch (err) {\n return {\n fileName: attachment.fileName,\n ok: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.endsWith(\"/\") ? url.slice(0, -1) : url;\n}\n\nfunction extractId(payload: unknown, key: \"incidentId\" | \"cardId\"): string | undefined {\n if (payload && typeof payload === \"object\" && key in payload) {\n const value = (payload as Record<string, unknown>)[key];\n if (value !== undefined && value !== null) return String(value);\n }\n return undefined;\n}\n\nasync function readJson(res: Response): Promise<unknown> {\n const text = await res.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction formatErrorBody(status: number, statusText: string, body: unknown): string {\n const prefix = `${status} ${statusText}`.trim();\n if (body && typeof body === \"object\" && \"error\" in body) {\n return `${prefix}: ${String((body as { error: unknown }).error)}`;\n }\n if (typeof body === \"string\" && body.length > 0) {\n return `${prefix}: ${body}`;\n }\n return prefix;\n}\n\nfunction toBase64(content: Uint8Array | string): string {\n const bytes = typeof content === \"string\" ? new TextEncoder().encode(content) : content;\n return bytesToBase64(bytes);\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(bytes).toString(\"base64\");\n }\n let binary = \"\";\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return btoa(binary);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-incidents",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Typed TypeScript client for Conveyor's public incidents REST API",
5
5
  "keywords": [
6
6
  "client",
@@ -10,7 +10,8 @@
10
10
  ],
11
11
  "license": "MIT",
12
12
  "files": [
13
- "dist"
13
+ "dist",
14
+ "README.md"
14
15
  ],
15
16
  "type": "module",
16
17
  "main": "./dist/index.js",