@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 +30 -5
- package/dist/client.d.ts +12 -1
- package/dist/errors.d.ts +10 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +78 -9
- package/dist/middleware.d.ts +6 -0
- package/dist/retry.d.ts +8 -1
- package/dist/types.d.ts +9 -0
- package/dist/webhooks.d.ts +3 -3
- package/package.json +1 -1
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
|
|
93
|
-
|
|
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
|
|
103
|
-
npm publish --access public
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
package/dist/middleware.d.ts
CHANGED
|
@@ -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:
|
|
22
|
+
export declare function retryingFetch(inner: FetchLike, options?: RetryOptions): typeof fetch;
|
package/dist/types.d.ts
ADDED
|
@@ -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];
|
package/dist/webhooks.d.ts
CHANGED
|
@@ -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
|
|
24
|
-
* `UnknownWebhookEventError` for an event this version
|
|
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>;
|