@prism-draft/sdk 0.1.0 → 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
@@ -89,21 +89,46 @@ redistribute its code. Using it to call the PrismDraft API is what it is for.
89
89
 
90
90
  ## Releasing
91
91
 
92
- Published from `main`, by a maintainer logged in to npm (`npm whoami`) with publish rights
93
- on the `@prism-draft` scope and a 2FA device.
92
+ Published from `main` by a maintainer who owns or belongs to the `prism-draft` npm
93
+ organisation and has a second factor that npm still accepts.
94
+
95
+ **One-time setup (done for `argus416`)**
96
+
97
+ 1. Create the organisation `prism-draft` at <https://www.npmjs.com/org/create> (the free
98
+ plan covers public packages). The scope `@prism-draft` exists only after this; before
99
+ it, `npm publish` fails with `Scope not found`.
100
+ 2. Enable two-factor authentication on the publishing account, set to "Authorization and
101
+ publishing". npm no longer accepts new authenticator-app (TOTP) enrolments
102
+ (`Adding a new TOTP 2FA is no longer supported`): add a passkey or security key at
103
+ `https://npmjs.com/settings/<user>/tfa`. Touch ID through the browser's passkey prompt is
104
+ enough. Without 2FA the registry answers `403 Two-factor authentication or granular access
105
+ token with bypass 2fa enabled is required to publish packages`.
106
+ 3. Log in: `npm login`, then check `npm whoami` and `npm org ls prism-draft`.
107
+
108
+ Tokens that bypass 2FA are being restricted by npm, so do not build the release around one.
109
+
110
+ **Each release**
94
111
 
95
112
  ```sh
96
113
  git switch main && git pull
97
114
  # bump "version" in packages/sdk/package.json (0.x: minor for new API surface or
98
115
  # SDK changes, patch for fixes), commit it, and merge it through a pull request
99
- bun install
116
+ bun install # a stale install fails the build: "Cannot find module 'openapi-fetch'"
100
117
  bun run --cwd apps/api openapi:export && bun run --cwd packages/sdk generate # must leave no diff
101
118
  cd packages/sdk && bun --env-file=../../.env test
102
- npm publish --dry-run # `prepublishOnly` builds; check the file list: dist/, README, package.json
103
- npm publish --access public # asks for the 2FA code
119
+ npm publish --dry-run # `prepublishOnly` builds; check the file list: dist/, README, package.json
120
+ npm publish --access public # prints a URL; approve it with the passkey
104
121
  git tag sdk-v<version> && git push origin sdk-v<version>
105
122
  ```
106
123
 
124
+ `bun publish --dry-run --access public` produces the same 14-file tarball (Bun 1.4.2 runs
125
+ `prepublishOnly`, `prepack` and `postpack`). Use `npm publish` for the real release: its
126
+ passkey approval flow is the one that has been exercised.
127
+
128
+ Afterwards, check `npm view @prism-draft/sdk version` and install it in an empty directory
129
+ (`bun add @prism-draft/sdk`, then import `PrismDraft`). The registry can take a minute to
130
+ show a new version.
131
+
107
132
  A published version cannot be replaced. Fix forward with a patch release; `npm deprecate`
108
133
  marks a bad one.
109
134
 
package/dist/client.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { PrismDraftResources } from "./generated/resources.js";
2
+ import { type FetchLike } from "./retry.js";
2
3
  import type { PrismDraftClient } from "./transport.js";
3
4
  export declare const DEFAULT_BASE_URL = "https://api.prism-draft.com";
4
5
  export interface PrismDraftOptions {
@@ -6,7 +7,17 @@ export interface PrismDraftOptions {
6
7
  apiKey: string;
7
8
  /** An absolute `http(s)` URL; default `https://api.prism-draft.com`. */
8
9
  baseUrl?: string;
9
- fetch?: typeof fetch;
10
+ /**
11
+ * Replaces the global `fetch`. A plain function is enough (no `preconnect`, which Bun's
12
+ * `typeof fetch` requires); the SDK calls it with a `Request`, and with `requestInit` after it.
13
+ */
14
+ fetch?: FetchLike;
15
+ /**
16
+ * Passed as the second argument of every `fetch` call, retries included: `{ cache: "no-store" }`
17
+ * for Next.js, a `dispatcher` for undici. `body`, `method`, `headers` and `signal` belong to
18
+ * the client and are ignored if present.
19
+ */
20
+ requestInit?: Omit<RequestInit, "body" | "method" | "headers" | "signal">;
10
21
  /** Retries for GET/HEAD on network errors and 429/502/503/504. Default 2; 0 disables. */
11
22
  maxRetries?: number;
12
23
  /**
package/dist/errors.d.ts CHANGED
@@ -18,6 +18,16 @@ export declare class PrismDraftTimeoutError extends Error {
18
18
  export declare class WebhookSignatureError extends Error {
19
19
  constructor(message?: string);
20
20
  }
21
+ /**
22
+ * A delivery with a valid signature whose body is not what the API sends: `reason` is `not_json`
23
+ * or `not_envelope` (JSON, but not `{ event, data }`). A `WebhookSignatureError` too, so a caller
24
+ * that catches that class keeps working; test for this class first to answer 400 here and 401 for
25
+ * the plain signature error.
26
+ */
27
+ export declare class WebhookPayloadError extends WebhookSignatureError {
28
+ readonly reason: "not_json" | "not_envelope";
29
+ constructor(reason: WebhookPayloadError["reason"], message: string);
30
+ }
21
31
  /** A signed delivery for an event this SDK version does not know. Answer 2xx and ignore it. */
22
32
  export declare class UnknownWebhookEventError extends Error {
23
33
  readonly event: string;
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  export { DEFAULT_BASE_URL, PrismDraft, type PrismDraftOptions } from "./client.js";
2
2
  export type { PrismDraftClient, RequestOptions } from "./transport.js";
3
+ export type { Article, ArticleStatus, ArticleVersion, Category } from "./types.js";
3
4
  export * from "./generated/resources.js";
4
5
  export type { paths, components, operations } from "./generated/schema.js";
5
- export { PrismDraftError, PrismDraftTimeoutError, UnknownWebhookEventError, WebhookSignatureError, } from "./errors.js";
6
+ export { PrismDraftError, PrismDraftTimeoutError, UnknownWebhookEventError, WebhookPayloadError, WebhookSignatureError, } from "./errors.js";
6
7
  export { throwOnError } from "./middleware.js";
7
- export { retryingFetch, type RetryOptions } from "./retry.js";
8
+ export { retryingFetch, type FetchLike, type RetryOptions } from "./retry.js";
8
9
  export { WEBHOOK_SIGNATURE_HEADER, constructWebhookEvent, verifyWebhookSignature, type VerifyInput, } from "./webhooks.js";
9
10
  export { WEBHOOK_EVENTS, type ArticleChangedField, type WebhookEvent, type WebhookEventData, type WebhookEventName, } from "./webhook-events.js";
package/dist/index.js CHANGED
@@ -1530,6 +1530,15 @@ class WebhookSignatureError extends Error {
1530
1530
  }
1531
1531
  }
1532
1532
 
1533
+ class WebhookPayloadError extends WebhookSignatureError {
1534
+ reason;
1535
+ constructor(reason, message) {
1536
+ super(message);
1537
+ this.name = "WebhookPayloadError";
1538
+ this.reason = reason;
1539
+ }
1540
+ }
1541
+
1533
1542
  class UnknownWebhookEventError extends Error {
1534
1543
  event;
1535
1544
  constructor(event) {
@@ -1569,6 +1578,21 @@ var rejectUnsafePathParams = {
1569
1578
  return;
1570
1579
  }
1571
1580
  };
1581
+ var rejectNonJsonBody = {
1582
+ async onResponse({ response }) {
1583
+ if (!response.ok)
1584
+ return;
1585
+ const text = await response.clone().text();
1586
+ if (text.length === 0)
1587
+ return;
1588
+ try {
1589
+ JSON.parse(text);
1590
+ } catch {
1591
+ throw new PrismDraftError(response.status, "INVALID_RESPONSE", "The API returned a non-JSON body");
1592
+ }
1593
+ return;
1594
+ }
1595
+ };
1572
1596
 
1573
1597
  // src/retry.ts
1574
1598
  var RETRYABLE_STATUS = new Set([429, 502, 503, 504]);
@@ -1613,6 +1637,34 @@ function wait(sleep, ms, signal) {
1613
1637
  sleep(ms, signal).then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
1614
1638
  });
1615
1639
  }
1640
+ function releaseWhenSettled(response, release) {
1641
+ if (response.body === null) {
1642
+ release();
1643
+ return response;
1644
+ }
1645
+ const reader = response.body.getReader();
1646
+ const body = new ReadableStream({
1647
+ async pull(controller) {
1648
+ try {
1649
+ const { done, value } = await reader.read();
1650
+ if (done) {
1651
+ release();
1652
+ controller.close();
1653
+ } else {
1654
+ controller.enqueue(value);
1655
+ }
1656
+ } catch (error) {
1657
+ release();
1658
+ controller.error(error);
1659
+ }
1660
+ },
1661
+ cancel(reason) {
1662
+ release();
1663
+ return reader.cancel(reason);
1664
+ }
1665
+ });
1666
+ return new Response(body, response);
1667
+ }
1616
1668
  function retryingFetch(inner, options = {}) {
1617
1669
  const maxRetries = options.maxRetries ?? 2;
1618
1670
  const sleep = options.sleep ?? defaultSleep;
@@ -1635,20 +1687,27 @@ function retryingFetch(inner, options = {}) {
1635
1687
  timeout = expiry;
1636
1688
  timer = setTimeout(() => expiry.abort(new PrismDraftTimeoutError(timeoutMs)), Math.min(timeoutMs, MAX_TIMER_MS));
1637
1689
  timer.unref?.();
1638
- request = new Request(request, { signal: anySignal([input.signal, expiry.signal]) });
1690
+ try {
1691
+ request = new Request(request, { signal: anySignal([input.signal, expiry.signal]) });
1692
+ } catch (error) {
1693
+ clearTimeout(timer);
1694
+ throw error;
1695
+ }
1639
1696
  }
1697
+ const release = () => clearTimeout(timer);
1640
1698
  const backoff = Math.min(BASE_WAIT_MS * 2 ** attempt, maxDelay);
1641
1699
  let delay;
1642
1700
  try {
1643
1701
  const response = await inner(request);
1644
1702
  if (!idempotent || !RETRYABLE_STATUS.has(response.status) || attempt >= maxRetries) {
1645
- return response;
1703
+ return releaseWhenSettled(response, release);
1646
1704
  }
1647
1705
  const asked = retryAfterMs(response);
1648
1706
  if (asked !== undefined && asked > maxDelay)
1649
- return response;
1650
- if (response.status === 429 && asked === undefined)
1651
- return response;
1707
+ return releaseWhenSettled(response, release);
1708
+ if (response.status === 429 && asked === undefined) {
1709
+ return releaseWhenSettled(response, release);
1710
+ }
1652
1711
  delay = asked ?? backoff;
1653
1712
  } catch (error) {
1654
1713
  clearTimeout(timer);
@@ -1675,6 +1734,15 @@ function isHttpUrl(value) {
1675
1734
  return false;
1676
1735
  }
1677
1736
  }
1737
+ var RESERVED_INIT_KEYS = ["body", "method", "headers", "signal"];
1738
+ function withRequestInit(inner, requestInit) {
1739
+ if (requestInit === undefined)
1740
+ return inner;
1741
+ const init = { ...requestInit };
1742
+ for (const key of RESERVED_INIT_KEYS)
1743
+ delete init[key];
1744
+ return (input) => inner(input, init);
1745
+ }
1678
1746
 
1679
1747
  class PrismDraft extends PrismDraftResources {
1680
1748
  raw;
@@ -1689,13 +1757,13 @@ class PrismDraft extends PrismDraftResources {
1689
1757
  const raw = createOpenApiClient({
1690
1758
  baseUrl,
1691
1759
  headers: { authorization: `Bearer ${options.apiKey}` },
1692
- fetch: retryingFetch(options.fetch ?? fetch, {
1760
+ fetch: retryingFetch(withRequestInit(options.fetch ?? fetch, options.requestInit), {
1693
1761
  maxRetries: options.maxRetries ?? 2,
1694
1762
  timeoutMs: options.timeoutMs,
1695
1763
  maxRetryDelayMs: options.maxRetryDelayMs
1696
1764
  })
1697
1765
  });
1698
- raw.use(rejectUnsafePathParams, throwOnError);
1766
+ raw.use(rejectUnsafePathParams, throwOnError, rejectNonJsonBody);
1699
1767
  super(raw);
1700
1768
  this.raw = raw;
1701
1769
  }
@@ -1803,11 +1871,11 @@ async function constructWebhookEvent(input) {
1803
1871
  try {
1804
1872
  envelope = JSON.parse(text);
1805
1873
  } catch {
1806
- throw new WebhookSignatureError("The webhook body is not JSON");
1874
+ throw new WebhookPayloadError("not_json", "The webhook body is not JSON");
1807
1875
  }
1808
1876
  const { event, data } = envelope ?? {};
1809
1877
  if (typeof event !== "string" || typeof data !== "object" || data === null) {
1810
- throw new WebhookSignatureError("The webhook body is not an { event, data } envelope");
1878
+ throw new WebhookPayloadError("not_envelope", "The webhook body is not an { event, data } envelope");
1811
1879
  }
1812
1880
  if (!WEBHOOK_EVENTS.includes(event)) {
1813
1881
  throw new UnknownWebhookEventError(event);
@@ -1847,6 +1915,7 @@ export {
1847
1915
  Usage,
1848
1916
  WEBHOOK_EVENTS,
1849
1917
  WEBHOOK_SIGNATURE_HEADER,
1918
+ WebhookPayloadError,
1850
1919
  WebhookSignatureError,
1851
1920
  Webhooks,
1852
1921
  Workspaces,
@@ -7,3 +7,9 @@ export declare const throwOnError: Middleware;
7
7
  * `/api/articles/..` into `/api/`). It is refused before any request is made.
8
8
  */
9
9
  export declare const rejectUnsafePathParams: Middleware;
10
+ /**
11
+ * A 2xx answer whose body is not JSON (an HTML error page from a proxy, say) becomes a
12
+ * `PrismDraftError`; `openapi-fetch` would throw a bare `SyntaxError`. The message carries
13
+ * neither the body nor the URL.
14
+ */
15
+ export declare const rejectNonJsonBody: Middleware;
package/dist/retry.d.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  /** The default for `maxRetryDelayMs`. A server that asks for more is not retried: see `retryAfterMs`. */
2
2
  export declare const DEFAULT_MAX_RETRY_DELAY_MS = 8000;
3
+ /**
4
+ * What the client needs of `fetch`: a call with a `Request`, `URL` or string and optional init.
5
+ * Looser than `typeof fetch`, which under Bun's types also demands `preconnect`, so a plain
6
+ * wrapper function is accepted without a cast. The SDK itself only calls `fetch(request)`
7
+ * (and `fetch(request, requestInit)` when the client has a `requestInit`).
8
+ */
9
+ export type FetchLike = (input: Request | URL | string, init?: RequestInit) => Promise<Response>;
3
10
  export interface RetryOptions {
4
11
  maxRetries?: number;
5
12
  /** Fails an attempt after this many ms with a `PrismDraftTimeoutError`; a request's own `timeoutMs` overrides it. Unset: none. */
@@ -12,4 +19,4 @@ export interface RetryOptions {
12
19
  /** One signal that aborts with the reason of whichever of `signals` aborts first. */
13
20
  export declare function anySignal(signals: AbortSignal[]): AbortSignal;
14
21
  /** Retries idempotent reads only; a write that may have landed is never replayed (decision D6). */
15
- export declare function retryingFetch(inner: typeof fetch, options?: RetryOptions): typeof fetch;
22
+ export declare function retryingFetch(inner: FetchLike, options?: RetryOptions): typeof fetch;
@@ -0,0 +1,9 @@
1
+ import type { ArticlesGetOutput, ArticlesListOutput } from "./generated/resources.js";
2
+ /** An article as `articles.list` returns it. */
3
+ export type Article = ArticlesListOutput["articles"][number];
4
+ /** `DRAFT`, `QUEUED`, `PROCESSING`, `NEEDS_REVIEW`, `CHANGES_REQUESTED`, `APPROVED` or `PUBLISHED`. */
5
+ export type ArticleStatus = Article["status"];
6
+ /** A category as it is embedded in an article of `articles.list`. */
7
+ export type Category = Article["categories"][number];
8
+ /** A language version of an article, as listed under `translations.versions` by `articles.get`. */
9
+ export type ArticleVersion = ArticlesGetOutput["translations"]["versions"][number];
@@ -20,8 +20,8 @@ export interface VerifyInput {
20
20
  export declare function verifyWebhookSignature(input: VerifyInput): Promise<boolean>;
21
21
  /**
22
22
  * Verifies a delivery and returns it typed. Throws `WebhookSignatureError` for a
23
- * bad signature or a body that is not `{ event, data }`, and
24
- * `UnknownWebhookEventError` for an event this version does not know: answer 2xx
25
- * to that one, or the API retries it.
23
+ * bad or stale signature, `WebhookPayloadError` (a subclass of it) for a signed body that is
24
+ * not JSON or not `{ event, data }`, and `UnknownWebhookEventError` for an event this version
25
+ * does not know: answer 2xx to that one, or the API retries it.
26
26
  */
27
27
  export declare function constructWebhookEvent(input: VerifyInput): Promise<WebhookEvent>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prism-draft/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Typed TypeScript client for the PrismDraft API, with webhook verification.",
5
5
  "keywords": [
6
6
  "api-client",