@nohead/sdk 0.0.0 → 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.
@@ -0,0 +1,99 @@
1
+ import type * as G from "./generated/types.gen.ts";
2
+ export type * from "./generated/schemas.gen.ts";
3
+ /**
4
+ * A record whose `data` has the shape `D`. Exported as `Nohead.Record` and
5
+ * `NoheadRecord`, so it never shadows TypeScript's own `Record<K, V>`.
6
+ *
7
+ * interface Post { title: string; author: string }
8
+ * const post = await nohead.records.get<Post>(id) // post.data.title
9
+ */
10
+ export type NoheadRecord<D = G.RecordData> = Omit<G.Record, "data"> & {
11
+ data: D;
12
+ };
13
+ /** Field values to write: `null` clears a field. */
14
+ export type RecordDataUpdate<D> = {
15
+ [K in keyof D]?: D[K] | null;
16
+ };
17
+ export type FilterValue = string | number | boolean | Date;
18
+ /**
19
+ * Equality filters: `{ status: "published", author: "rec_…" }`. For fields
20
+ * with several values a filter means "contains".
21
+ */
22
+ export interface RecordFilter {
23
+ status?: "draft" | "published";
24
+ deleted?: boolean;
25
+ [apiKey: string]: FilterValue | undefined;
26
+ }
27
+ export type RecordSort = NonNullable<NonNullable<G.RecordsListData["query"]>["sort"]>;
28
+ export interface ListParams {
29
+ /** Page size, 1 to 100 (default 20). */
30
+ limit?: number;
31
+ /** Start from a page's `meta.next_cursor`. */
32
+ cursor?: string;
33
+ }
34
+ export interface RecordListParams extends ListParams {
35
+ filter?: RecordFilter;
36
+ sort?: RecordSort;
37
+ /** Relation or asset fields to embed under `expanded` (at most 5). */
38
+ expand?: string[];
39
+ }
40
+ export interface RecordGetParams {
41
+ expand?: string[];
42
+ include_deleted?: boolean;
43
+ }
44
+ export interface RecordCreateParams<D = G.RecordData> {
45
+ data: D;
46
+ }
47
+ export interface RecordUpdateParams<D = G.RecordData> {
48
+ data: RecordDataUpdate<D>;
49
+ }
50
+ export interface RecordCountParams {
51
+ filter?: RecordFilter;
52
+ }
53
+ export interface RecordScheduleParams {
54
+ /** A future time, or `null` for none. Omitted keys keep their value. */
55
+ publish_at?: string | Date | null;
56
+ unpublish_at?: string | Date | null;
57
+ }
58
+ export type RecordBulkParams = G.BulkRecordsRequest;
59
+ export type RecordDiffParams = NonNullable<G.RecordsDiffData["query"]>;
60
+ export interface RecordSearchParams extends ListParams {
61
+ /** Needs `filterable` fields (status and timestamps always work). */
62
+ filter?: RecordFilter;
63
+ /** Needs a `sortable` field, e.g. `-published_at`; relevance otherwise. */
64
+ sort?: string;
65
+ expand?: string[];
66
+ }
67
+ export interface ProjectSearchParams extends ListParams {
68
+ /** Collection slugs or IDs to search in; every searchable one otherwise. */
69
+ collections?: string[];
70
+ filter?: {
71
+ status?: "draft" | "published";
72
+ };
73
+ }
74
+ export interface RevisionListParams extends ListParams {
75
+ filter?: NonNullable<G.RecordRevisionsListData["query"]>["filter"];
76
+ }
77
+ export interface DryRunParams {
78
+ /** Preview the change without making it. */
79
+ dry_run?: boolean;
80
+ }
81
+ export interface DeletedListParams extends ListParams {
82
+ /** List deleted items instead (they can be restored for 30 days). */
83
+ deleted?: boolean;
84
+ }
85
+ export interface SchemaParams {
86
+ /** A past schema version; the current one otherwise. */
87
+ version?: number;
88
+ }
89
+ export type FieldMigrateParams = G.FieldMigrationRequest;
90
+ export type ImageUrlParams = NonNullable<G.AssetsImageUrlData["query"]>;
91
+ export interface DeliveryListParams extends ListParams {
92
+ status?: "pending" | "succeeded" | "failed";
93
+ }
94
+ export interface AuditEventListParams extends ListParams {
95
+ filter?: Omit<NonNullable<NonNullable<G.AuditEventsListForProjectData["query"]>["filter"]>, "project_id">;
96
+ sort?: "id" | "-id";
97
+ }
98
+ /** The metadata of a search page: also an estimate of the total hits. */
99
+ export type SearchMeta = G.SearchResults["meta"];
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare const VERSION = "0.1.0";
@@ -0,0 +1 @@
1
+ export const VERSION = "0.1.0";
@@ -0,0 +1,47 @@
1
+ import type { Asset, Collection, SchemaChange, Webhook } from "./generated/types.gen.ts";
2
+ import type { NoheadRecord, RecordData } from "./types.ts";
3
+ interface EventBase<Type extends string, Data> {
4
+ id: string;
5
+ object: "event";
6
+ type: Type;
7
+ created_at: string;
8
+ project_id: string;
9
+ data: Data;
10
+ }
11
+ export type RecordEvent<D = RecordData> = EventBase<"record.created" | "record.updated" | "record.deleted" | "record.published" | "record.unpublished", {
12
+ record: NoheadRecord<D>;
13
+ revision: number;
14
+ revision_id: string;
15
+ }>;
16
+ export type AssetEvent = EventBase<"asset.created" | "asset.deleted", {
17
+ asset: Asset;
18
+ }>;
19
+ export type SchemaChangedEvent = EventBase<"schema.changed", {
20
+ collection: Collection;
21
+ schema_change: SchemaChange;
22
+ }>;
23
+ /** Sent by `webhooks.test`. */
24
+ export type WebhookTestEvent = EventBase<"webhook.test", {
25
+ webhook: Webhook;
26
+ }>;
27
+ /** A webhook request's body, narrowed by `type`. */
28
+ export type WebhookEvent<D = RecordData> = RecordEvent<D> | AssetEvent | SchemaChangedEvent | WebhookTestEvent;
29
+ /** Request headers: a `Headers` object or a plain object (e.g. Node's). */
30
+ export type WebhookHeaders = Headers | Record<string, string | string[] | undefined>;
31
+ export interface UnwrapOptions {
32
+ /** The webhook's signing secret (`whsec_…`). */
33
+ secret: string;
34
+ /** Seconds a timestamp may be off from now. Default 300. */
35
+ tolerance?: number;
36
+ }
37
+ /**
38
+ * Checks a webhook request's signature and timestamp and returns its event.
39
+ * Pass the raw body exactly as received: parsing and re-serializing JSON
40
+ * changes the bytes, and the signature with them. Throws
41
+ * `WebhookVerificationError` when anything does not match.
42
+ *
43
+ * const event = await unwrapWebhook(await request.text(), request.headers, { secret })
44
+ * if (event.type === "record.published") { … }
45
+ */
46
+ export declare function unwrapWebhook<D = RecordData>(body: string | ArrayBuffer | Uint8Array, headers: WebhookHeaders, { secret, tolerance }: UnwrapOptions): Promise<WebhookEvent<D>>;
47
+ export {};
@@ -0,0 +1,70 @@
1
+ // Verifying webhook requests (Standard Webhooks), without a client:
2
+ //
3
+ // import { unwrapWebhook } from "@nohead/sdk/webhooks"
4
+ import { WebhookVerificationError } from "./errors.js";
5
+ /**
6
+ * Checks a webhook request's signature and timestamp and returns its event.
7
+ * Pass the raw body exactly as received: parsing and re-serializing JSON
8
+ * changes the bytes, and the signature with them. Throws
9
+ * `WebhookVerificationError` when anything does not match.
10
+ *
11
+ * const event = await unwrapWebhook(await request.text(), request.headers, { secret })
12
+ * if (event.type === "record.published") { … }
13
+ */
14
+ export async function unwrapWebhook(body, headers, { secret, tolerance = 300 }) {
15
+ const id = header(headers, "webhook-id");
16
+ const timestamp = header(headers, "webhook-timestamp");
17
+ const signatures = header(headers, "webhook-signature");
18
+ if (!id || !timestamp || !signatures) {
19
+ throw new WebhookVerificationError("Missing webhook-id, webhook-timestamp or webhook-signature header");
20
+ }
21
+ const sentAt = Number(timestamp);
22
+ if (!Number.isInteger(sentAt) ||
23
+ Math.abs(Date.now() / 1000 - sentAt) > tolerance) {
24
+ throw new WebhookVerificationError("The webhook timestamp is too far from the current time");
25
+ }
26
+ const text = typeof body === "string" ? body : new TextDecoder().decode(body);
27
+ const key = await crypto.subtle.importKey("raw", decodeBase64(secret.replace(/^whsec_/, ""), "secret"), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
28
+ const signed = new TextEncoder().encode(`${id}.${timestamp}.${text}`);
29
+ for (const entry of signatures.split(" ")) {
30
+ const [version, signature] = entry.split(",", 2);
31
+ if (version !== "v1" || !signature)
32
+ continue;
33
+ let bytes;
34
+ try {
35
+ bytes = decodeBase64(signature, "signature");
36
+ }
37
+ catch {
38
+ continue;
39
+ }
40
+ // Constant-time comparison: the platform verifies the HMAC.
41
+ if (await crypto.subtle.verify("HMAC", key, bytes, signed)) {
42
+ try {
43
+ return JSON.parse(text);
44
+ }
45
+ catch {
46
+ throw new WebhookVerificationError("The webhook body is not JSON");
47
+ }
48
+ }
49
+ }
50
+ throw new WebhookVerificationError("No webhook signature matches");
51
+ }
52
+ function header(headers, name) {
53
+ if (headers instanceof Headers)
54
+ return headers.get(name) ?? undefined;
55
+ const entry = Object.entries(headers).find(([key]) => key.toLowerCase() === name)?.[1];
56
+ return Array.isArray(entry) ? entry.join(" ") : entry;
57
+ }
58
+ function decodeBase64(value, what) {
59
+ let binary;
60
+ try {
61
+ binary = atob(value);
62
+ }
63
+ catch {
64
+ throw new WebhookVerificationError(`The webhook ${what} is not base64`);
65
+ }
66
+ const bytes = new Uint8Array(binary.length);
67
+ for (let i = 0; i < binary.length; i++)
68
+ bytes[i] = binary.charCodeAt(i);
69
+ return bytes;
70
+ }
package/package.json CHANGED
@@ -1,8 +1,63 @@
1
1
  {
2
2
  "name": "@nohead/sdk",
3
- "version": "0.0.0",
4
- "description": "Placeholder for the official TypeScript SDK for the Nohead API. The first real release is 0.1.0.",
3
+ "version": "0.1.0",
4
+ "description": "Official TypeScript SDK for the Nohead API",
5
5
  "license": "MIT",
6
6
  "homepage": "https://nohead.io",
7
- "files": ["README.md"]
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/nohead-io/nohead-typescript.git"
10
+ },
11
+ "bugs": "https://github.com/nohead-io/nohead-typescript/issues",
12
+ "keywords": [
13
+ "nohead",
14
+ "cms",
15
+ "headless",
16
+ "api",
17
+ "sdk"
18
+ ],
19
+ "type": "module",
20
+ "sideEffects": false,
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./webhooks": {
27
+ "types": "./dist/webhooks.d.ts",
28
+ "default": "./dist/webhooks.js"
29
+ },
30
+ "./package.json": "./package.json"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "CHANGELOG.md"
35
+ ],
36
+ "engines": {
37
+ "node": ">=22"
38
+ },
39
+ "scripts": {
40
+ "generate": "openapi-ts && node scripts/operations.ts",
41
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
42
+ "prepare": "npm run build",
43
+ "typecheck": "tsc",
44
+ "lint": "eslint .",
45
+ "format": "prettier --write .",
46
+ "format:check": "prettier --check .",
47
+ "test": "vitest run",
48
+ "smoke": "node smoke/smoke.ts",
49
+ "samples": "node scripts/samples.ts",
50
+ "typecheck:smoke": "tsc -p smoke"
51
+ },
52
+ "devDependencies": {
53
+ "@eslint/js": "^10.0.1",
54
+ "@hey-api/openapi-ts": "0.99.0",
55
+ "@types/node": "^26.6.4",
56
+ "eslint": "^10.12.0",
57
+ "globals": "^17.13.0",
58
+ "prettier": "^3.9.9",
59
+ "typescript": "^6.0.3",
60
+ "typescript-eslint": "^8.71.0",
61
+ "vitest": "^5.0.3"
62
+ }
8
63
  }