@forumulate/sdk 0.1.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/dist/schema.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ /**
3
+ * This file was auto-generated by openapi-typescript.
4
+ * Do not make direct changes to the file.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":";AAAA;;;GAGG"}
@@ -0,0 +1,121 @@
1
+ import type { paths } from './schema';
2
+ export type { paths };
3
+ /** HTTP verbs a generated path entry can carry (openapi-typescript's key set). */
4
+ export type HttpMethod = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch' | 'trace';
5
+ /**
6
+ * The verbs that actually exist on a path. openapi-typescript stamps every
7
+ * absent verb as `put?: never`, so we drop the `never` slots to leave real
8
+ * operations only — this is what powers method autocomplete on `typed()`.
9
+ */
10
+ export type MethodsOf<P extends keyof paths> = {
11
+ [M in keyof paths[P]]: paths[P][M] extends never ? never : M extends HttpMethod ? M : never;
12
+ }[keyof paths[P]];
13
+ /** The generated operation object for a `(path, method)` pair. */
14
+ export type OperationOf<P extends keyof paths, M extends keyof paths[P]> = paths[P][M];
15
+ /** `true` when `T` has at least one key (used to decide required vs. optional). */
16
+ type HasKeys<T> = [keyof T] extends [never] ? false : true;
17
+ /** The `params` slot: required when the route has path params, absent otherwise. */
18
+ type ParamsPart<Op> = Op extends {
19
+ parameters: {
20
+ path: infer P;
21
+ };
22
+ } ? HasKeys<P> extends true ? {
23
+ params: P;
24
+ } : {
25
+ params?: undefined;
26
+ } : {
27
+ params?: undefined;
28
+ };
29
+ /**
30
+ * The `query` slot: present when the route declares query params, and required
31
+ * only when the route has at least one REQUIRED one.
32
+ *
33
+ * The required pattern is tested first, then the optional one — the same order
34
+ * and for the same reason as `BodyPart` below. openapi-typescript emits
35
+ * `query?: {…}` (an OPTIONAL property) whenever every query parameter on the
36
+ * route is itself optional, and an optional property does not satisfy
37
+ * `{ query: infer Q }`. Matching only the required form therefore fell through
38
+ * to `{ query?: undefined }` and made `typed()` REFUSE a perfectly legal query
39
+ * argument on every such route — which is most of them: of 1,220 operations,
40
+ * the ones with all-optional query params are the common case.
41
+ *
42
+ * `GET /orgs/{orgId}/spaces` is the case that caught it: `deleted`, `limit` and
43
+ * `cursor` are all optional, so passing any of them was a type error even
44
+ * though the route has always accepted them. `ParamsPart` above is unaffected
45
+ * only by luck — a path parameter is never optional, so it is always emitted in
46
+ * the required form.
47
+ *
48
+ * `NonNullable` strips the `| undefined` that inferring through an optional
49
+ * property adds, so `HasKeys` sees the object rather than a union with
50
+ * `undefined` (which would otherwise always look like it has keys).
51
+ */
52
+ type QueryPart<Op> = Op extends {
53
+ parameters: {
54
+ query: infer Q;
55
+ };
56
+ } ? HasKeys<NonNullable<Q>> extends true ? {
57
+ query: NonNullable<Q>;
58
+ } : {
59
+ query?: undefined;
60
+ } : Op extends {
61
+ parameters: {
62
+ query?: infer Q;
63
+ };
64
+ } ? HasKeys<NonNullable<Q>> extends true ? {
65
+ query?: NonNullable<Q>;
66
+ } : {
67
+ query?: undefined;
68
+ } : {
69
+ query?: undefined;
70
+ };
71
+ /**
72
+ * The `body` slot. A required `requestBody` (no `?` in the schema) yields a
73
+ * required `body`; an optional one yields an optional `body`. The required
74
+ * pattern is tested first because an optional `requestBody` does not satisfy it.
75
+ */
76
+ type BodyPart<Op> = Op extends {
77
+ requestBody: {
78
+ content: {
79
+ 'application/json': infer B;
80
+ };
81
+ };
82
+ } ? {
83
+ body: B;
84
+ } : Op extends {
85
+ requestBody?: {
86
+ content: {
87
+ 'application/json': infer B;
88
+ };
89
+ };
90
+ } ? {
91
+ body?: B;
92
+ } : {
93
+ body?: undefined;
94
+ };
95
+ /** Flatten an intersection into a single object literal for readable hovers. */
96
+ type Simplify<T> = {
97
+ [K in keyof T]: T[K];
98
+ } & {};
99
+ /**
100
+ * The `init` argument for `typed(method, path, init)`, derived from the route:
101
+ * path params, query params and JSON body are each required exactly when the
102
+ * spec says so, plus a `raw` escape to get the untouched `Response` back.
103
+ */
104
+ export type TypedRequestInit<Op> = Simplify<ParamsPart<Op> & QueryPart<Op> & BodyPart<Op> & {
105
+ raw?: boolean;
106
+ }>;
107
+ /** Pull the `application/json` payload out of one response entry. */
108
+ type JsonBody<R> = R extends {
109
+ content: {
110
+ 'application/json': infer D;
111
+ };
112
+ } ? D : unknown;
113
+ /**
114
+ * The success payload for a route: the 200, then 201, then 202 body, or
115
+ * `undefined` for a 204. Routes whose spec response is empty resolve to
116
+ * `unknown` — the value is still whatever the API returns, just not narrowed.
117
+ */
118
+ export type TypedResponse<Op> = Op extends {
119
+ responses: infer R;
120
+ } ? 200 extends keyof R ? JsonBody<R[200]> : 201 extends keyof R ? JsonBody<R[201]> : 202 extends keyof R ? JsonBody<R[202]> : 204 extends keyof R ? undefined : unknown : unknown;
121
+ //# sourceMappingURL=typed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typed.d.ts","sourceRoot":"","sources":["../src/typed.ts"],"names":[],"mappings":"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"}
package/dist/typed.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ // Type-level machinery that turns the machine-generated `paths` interface in
3
+ // `schema.ts` into an ergonomic, fully-typed request signature. This is the
4
+ // bridge that gives the hand-written client typed coverage of the WHOLE REST
5
+ // API (every route in `openapi.json`) instead of only the ~30 curated helpers.
6
+ //
7
+ // Everything here is types-only — no runtime code, no runtime dependency. The
8
+ // generated `schema.ts` is likewise types-only, so adding it costs nothing at
9
+ // runtime and cannot drift from the client's behaviour.
10
+ //
11
+ // See `ForumulateClient.typed()` in `index.ts` for the method these types shape,
12
+ // and the header of `schema.ts` for how to regenerate the source `paths`.
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ //# sourceMappingURL=typed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typed.js","sourceRoot":"","sources":["../src/typed.ts"],"names":[],"mappings":";AAAA,6EAA6E;AAC7E,4EAA4E;AAC5E,6EAA6E;AAC7E,+EAA+E;AAC/E,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,wDAAwD;AACxD,EAAE;AACF,iFAAiF;AACjF,0EAA0E"}
@@ -0,0 +1,179 @@
1
+ export type Role = 'OWNER' | 'ADMIN' | 'MAINTAINER' | 'EDITOR' | 'VIEWER';
2
+ export type SpaceType = 'DOCS' | 'BLOG' | 'FORUM';
3
+ export type SpaceVisibility = 'PRIVATE' | 'PUBLIC';
4
+ export type WebhookFormat = 'json' | 'slack' | 'discord';
5
+ /** A ProseMirror/TipTap-style document node. */
6
+ export interface BlockNode {
7
+ type: string;
8
+ content?: BlockNode[];
9
+ text?: string;
10
+ attrs?: Record<string, unknown>;
11
+ marks?: {
12
+ type: string;
13
+ attrs?: Record<string, unknown>;
14
+ }[];
15
+ }
16
+ export interface BlockDoc {
17
+ type: 'doc';
18
+ content: BlockNode[];
19
+ }
20
+ export interface Organization {
21
+ id: string;
22
+ name: string;
23
+ slug: string;
24
+ }
25
+ export interface Space {
26
+ id: string;
27
+ name: string;
28
+ slug: string;
29
+ type: SpaceType;
30
+ visibility: SpaceVisibility;
31
+ }
32
+ export interface Collection {
33
+ id: string;
34
+ name: string;
35
+ slug: string;
36
+ }
37
+ export interface Page {
38
+ id: string;
39
+ title: string;
40
+ slug: string;
41
+ status?: 'draft' | 'published';
42
+ }
43
+ export interface SearchHit {
44
+ pageId: string;
45
+ title: string;
46
+ snippet: string;
47
+ }
48
+ /** A result from the org-wide unified search (docs, forum, blog). */
49
+ export interface UnifiedHit {
50
+ sourceType: 'docs' | 'topic' | 'blog';
51
+ id: string;
52
+ spaceId: string;
53
+ spaceName: string;
54
+ title: string;
55
+ snippet: string;
56
+ url: string;
57
+ score: number;
58
+ }
59
+ export interface Comment {
60
+ id: string;
61
+ parentId: string | null;
62
+ authorName: string;
63
+ body: string;
64
+ status: 'visible' | 'pending' | 'hidden';
65
+ reactions: Record<string, number>;
66
+ createdAt: string;
67
+ }
68
+ export interface Webhook {
69
+ id: string;
70
+ url: string;
71
+ events: string[];
72
+ format: WebhookFormat;
73
+ active: boolean;
74
+ }
75
+ export interface PublicPage {
76
+ title: string;
77
+ content: BlockDoc;
78
+ metadata: {
79
+ icon?: string;
80
+ coverUrl?: string;
81
+ layout?: 'default' | 'wide';
82
+ };
83
+ variables: Record<string, string>;
84
+ version: string;
85
+ locale: string;
86
+ }
87
+ /** What a shape says is still missing from a piece. Computed, never enforced. */
88
+ export interface Conformance {
89
+ conforms: boolean;
90
+ missing: string[];
91
+ extra: string[];
92
+ duplicate: string[];
93
+ }
94
+ /** One piece of a space — a page, a topic or an incident, depending on the space. */
95
+ export interface Item {
96
+ id: string;
97
+ kind: 'page' | 'topic' | 'incident';
98
+ title: string;
99
+ slug?: string;
100
+ conformance: Conformance;
101
+ /** When it last changed, ISO 8601. What a walk is ordered by. */
102
+ updatedAt?: string;
103
+ /** Markdown per named slot, when `include` asked for `sections`. */
104
+ sections?: Record<string, string>;
105
+ /** The whole piece as markdown, when `include` asked for `body`. */
106
+ markdown?: string;
107
+ /**
108
+ * WHICH of `sections` and `markdown` holds this piece's content.
109
+ *
110
+ * `sections` is populated only for a piece written against a shape, so a
111
+ * free-form piece returns `{}` — indistinguishable, without this, from a
112
+ * piece that is genuinely empty. Asking for `sections` alone and trusting an
113
+ * empty object is how a sync reports success having carried nothing.
114
+ */
115
+ contentIn?: 'sections' | 'markdown';
116
+ }
117
+ /** Where a walk through a collection continues from. */
118
+ export interface PageInfo {
119
+ has_more: boolean;
120
+ next_cursor?: string | null;
121
+ total?: number;
122
+ }
123
+ /**
124
+ * The shape types, mirrored rather than imported.
125
+ *
126
+ * This package is published on its own, so it cannot depend on the workspace
127
+ * package these are defined in — the same reason `Page` and `Space` are
128
+ * mirrored here. They are structural, so they stay assignable either way.
129
+ */
130
+ export interface ShapeSection {
131
+ key: string;
132
+ label: string;
133
+ kind: 'prose' | 'list' | 'steps' | 'table';
134
+ required?: boolean;
135
+ hint?: string;
136
+ example?: string;
137
+ schemaProperty?: string;
138
+ render?: 'labelled' | 'lede' | 'plain';
139
+ }
140
+ export interface ShapeField {
141
+ key: string;
142
+ label: string;
143
+ type: 'text' | 'select' | 'url' | 'date' | 'boolean';
144
+ options?: readonly string[];
145
+ hint?: string;
146
+ placeholder?: string;
147
+ }
148
+ /** What a space is: what a reader arrives at, how they move, who may add. */
149
+ export interface SpaceArchitecture {
150
+ index: 'landing' | 'list' | 'board' | 'single';
151
+ order: 'manual' | 'chronological' | 'activity' | 'votes' | 'status';
152
+ nav: 'tree' | 'categories' | 'tags' | 'sections' | 'none';
153
+ whoCreates: 'staff' | 'members' | 'anyone';
154
+ summary: readonly string[];
155
+ }
156
+ export interface EffectiveShape {
157
+ space: SpaceArchitecture;
158
+ params: ShapeField[];
159
+ sections: ShapeSection[];
160
+ /** Keys an override named that no longer exist. Reported, never fatal. */
161
+ unknown: string[];
162
+ }
163
+ /** A space's own changes, stored as a patch so upstream improvements still land. */
164
+ export interface ShapeOverride {
165
+ space?: Partial<SpaceArchitecture>;
166
+ sections?: {
167
+ add?: ShapeSection[];
168
+ omit?: string[];
169
+ order?: string[];
170
+ set?: Record<string, Partial<Omit<ShapeSection, 'key'>>>;
171
+ };
172
+ params?: {
173
+ add?: ShapeField[];
174
+ omit?: string[];
175
+ order?: string[];
176
+ set?: Record<string, Partial<Omit<ShapeField, 'key'>>>;
177
+ };
178
+ }
179
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ // Wire types for the Forumulate REST API. Kept intentionally lightweight and
3
+ // hand-curated for the stable surface; the full machine-readable schema is
4
+ // available from the running API at `/docs-json` (see README → Regenerating).
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Verifying a webhook delivery, as the receiver actually has to do it.
3
+ *
4
+ * The published contract said:
5
+ *
6
+ * X-Forumulate-Signature: v1,sha256=<hex>
7
+ *
8
+ * and a reader does the obvious thing with that — compute one value and
9
+ * compare the header to it. Every delivery to an endpoint still on the default
10
+ * `signatureVersion: 'both'`, and every delivery during a secret rotation,
11
+ * then fails to verify, because the real header is a comma-separated LIST:
12
+ *
13
+ * v1,sha256=<a>, v1,sha256=<b>, sha256=<c>, sha256=<d>
14
+ * ^ current secret, v1 ^ previous secret, v1
15
+ * ^ the legacy body-only signature, both secrets
16
+ *
17
+ * The sender was right and the document was wrong. Two secrets ride along
18
+ * through a rotation so nobody's verifier breaks on a day we choose, and the
19
+ * legacy body-only signature rides along until the endpoint's owner turns it
20
+ * off — and a receiver is meant to accept the delivery if ANY entry matches
21
+ * the one it computes. That is three facts a sentence in a reference page kept
22
+ * getting wrong, so here is the function instead.
23
+ *
24
+ * Verify `v1`. It signs `<timestamp>.<raw body>`, which is what makes a
25
+ * captured delivery un-replayable: the legacy `sha256=` covers the body alone
26
+ * and is therefore replayable forever. This accepts only `v1` unless you ask
27
+ * for the legacy one explicitly.
28
+ *
29
+ * import { verifyWebhookSignature } from '@forumulate/sdk';
30
+ *
31
+ * app.post('/hooks/forumulate', async (req, res) => {
32
+ * const ok = await verifyWebhookSignature({
33
+ * secret: process.env.FORUMULATE_WEBHOOK_SECRET!,
34
+ * // The RAW body, before any JSON parsing. A re-serialised body has
35
+ * // different bytes and will never verify.
36
+ * body: req.rawBody,
37
+ * signature: req.header('X-Forumulate-Signature'),
38
+ * timestamp: req.header('X-Forumulate-Timestamp'),
39
+ * });
40
+ * if (!ok) return res.sendStatus(401);
41
+ * // X-Forumulate-Delivery is the key to dedupe on — retries reuse it.
42
+ * res.sendStatus(200);
43
+ * });
44
+ *
45
+ * WebCrypto, so this is the same code in Node, Bun, Deno, Cloudflare Workers
46
+ * and a browser. It is async for that reason alone.
47
+ */
48
+ export interface VerifyWebhookInput {
49
+ /** The endpoint's signing secret. */
50
+ secret: string;
51
+ /** The raw request body, exactly as received — bytes, or the string of them. */
52
+ body: string | Uint8Array;
53
+ /** The `X-Forumulate-Signature` header value, whole. */
54
+ signature: string | null | undefined;
55
+ /** The `X-Forumulate-Timestamp` header value (unix seconds). */
56
+ timestamp: string | number | null | undefined;
57
+ /**
58
+ * How old a delivery may be, in seconds. Default 300.
59
+ *
60
+ * This is the part that makes the timestamp worth signing: without a window,
61
+ * a captured delivery verifies forever and the signature proves only that we
62
+ * sent it ONCE. Set it to 0 to skip the check (for a replay of a stored
63
+ * delivery in a test, say).
64
+ */
65
+ toleranceSeconds?: number;
66
+ /**
67
+ * Accept the legacy body-only `sha256=` signature as well. Default false.
68
+ *
69
+ * It is still sent while an endpoint's `signatureVersion` is `both`, and it
70
+ * is replayable by construction, so accepting it is a decision rather than a
71
+ * default.
72
+ */
73
+ acceptLegacy?: boolean;
74
+ /** Overridable for tests. Defaults to now. */
75
+ now?: () => number;
76
+ }
77
+ export declare function verifyWebhookSignature(input: VerifyWebhookInput): Promise<boolean>;
78
+ //# sourceMappingURL=webhooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,MAAM,WAAW,kBAAkB;IACjC,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC;IAC1B,wDAAwD;IACxD,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACrC,gEAAgE;IAChE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC9C;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,8CAA8C;IAC9C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAgCxF"}
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyWebhookSignature = verifyWebhookSignature;
4
+ async function verifyWebhookSignature(input) {
5
+ const { secret, signature, acceptLegacy = false, toleranceSeconds = 300 } = input;
6
+ if (!secret || !signature)
7
+ return false;
8
+ const timestamp = Number(input.timestamp);
9
+ if (!Number.isFinite(timestamp) || timestamp <= 0)
10
+ return false;
11
+ if (toleranceSeconds > 0) {
12
+ const now = Math.floor((input.now?.() ?? Date.now()) / 1000);
13
+ // Both directions: a delivery from the future is as suspicious as a stale
14
+ // one, and clock skew is why the window is seconds rather than exact.
15
+ if (Math.abs(now - timestamp) > toleranceSeconds)
16
+ return false;
17
+ }
18
+ const body = typeof input.body === 'string' ? input.body : new TextDecoder().decode(input.body);
19
+ // Entries, not an equality check: a rotation and the legacy overlap both put
20
+ // more than one signature in this header, and `header === expected` is the
21
+ // mistake the document invited.
22
+ const offered = signature.split(',').map((part) => part.trim().toLowerCase());
23
+ const expected = [`v1,sha256=${await hmacHex(secret, `${timestamp}.${body}`)}`];
24
+ if (acceptLegacy)
25
+ expected.push(`sha256=${await hmacHex(secret, body)}`);
26
+ for (const candidate of expected) {
27
+ // `v1,sha256=<hex>` contains the comma we just split on, so put it back.
28
+ const [prefix, value] = candidate.split('=', 2);
29
+ const wanted = `${prefix}=${value}`;
30
+ const rejoined = offered.join(',');
31
+ // Membership against the rejoined list rather than the split parts, so the
32
+ // `v1,` prefix survives the split that produced the parts.
33
+ if (!rejoined.includes(wanted))
34
+ continue;
35
+ if (timingSafeEqual(wanted, extractMatch(rejoined, wanted)))
36
+ return true;
37
+ }
38
+ return false;
39
+ }
40
+ /** The substring of `haystack` at `needle`'s position, for a constant-time compare. */
41
+ function extractMatch(haystack, needle) {
42
+ const at = haystack.indexOf(needle);
43
+ return at < 0 ? '' : haystack.slice(at, at + needle.length);
44
+ }
45
+ async function hmacHex(secret, message) {
46
+ const encoder = new TextEncoder();
47
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
48
+ const mac = await crypto.subtle.sign('HMAC', key, encoder.encode(message));
49
+ return [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join('');
50
+ }
51
+ /**
52
+ * Length-independent, content-constant-time comparison.
53
+ *
54
+ * Both sides here are hex of a fixed length, so this is belt and braces — but
55
+ * a verifier is exactly the place where "it does not matter here" ages badly.
56
+ */
57
+ function timingSafeEqual(a, b) {
58
+ if (a.length !== b.length)
59
+ return false;
60
+ let diff = 0;
61
+ for (let i = 0; i < a.length; i++)
62
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
63
+ return diff === 0;
64
+ }
65
+ //# sourceMappingURL=webhooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@forumulate/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Typed TypeScript client for the Forumulate REST API.",
5
+ "license": "MIT",
6
+ "homepage": "https://forumulate.com/reference",
7
+ "bugs": {
8
+ "url": "https://forumulate.com/community"
9
+ },
10
+ "keywords": [
11
+ "forumulate",
12
+ "docs",
13
+ "documentation",
14
+ "api",
15
+ "sdk",
16
+ "typescript",
17
+ "openapi",
18
+ "knowledge-base",
19
+ "forum"
20
+ ],
21
+ "main": "dist/index.js",
22
+ "types": "dist/index.d.ts",
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "devDependencies": {
32
+ "tsx": "^4.19.2",
33
+ "typescript": "^5.6.3",
34
+ "vitest": "^2.1.4",
35
+ "@forumulate/api": "0.0.0"
36
+ },
37
+ "author": "Forumulate",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.ts",
41
+ "default": "./dist/index.js"
42
+ }
43
+ },
44
+ "scripts": {
45
+ "build": "tsc -p tsconfig.json",
46
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
47
+ "test": "vitest run",
48
+ "dump-spec": "tsx scripts/dump-openapi.ts",
49
+ "generate-schema": "npx -y openapi-typescript@7 openapi.json -o src/schema.ts"
50
+ }
51
+ }