@cmssy/core 11.5.1 → 11.7.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.
@@ -1,48 +1,20 @@
1
1
  import { CmssyClientConfig, CmssyLayoutGroup, CmssyPageData, CmssyPageMeta, CmssyPageSummary } from '@cmssy/types';
2
2
 
3
- /** HTTP-level failure from the cmssy API, with a machine-readable status. */
4
3
  declare class CmssyRequestError extends Error {
5
4
  readonly status: number;
6
- /**
7
- * How long the server asked us to wait, in ms, when it said so. Present on a
8
- * 429 that carried `Retry-After`; a caller can serve stale content for that
9
- * long instead of guessing.
10
- */
11
5
  readonly retryAfterMs?: number;
12
6
  constructor(message: string, status: number, retryAfterMs?: number);
13
7
  }
14
8
  interface RetryPolicy {
15
- /** Additional attempts after the first request (default 3). */
16
9
  maxRetries?: number;
17
- /** Exponential backoff base in ms: base * 2^attempt (default 300). */
18
10
  baseDelayMs?: number;
19
- /** Upper bound for a guessed backoff wait (default 3000). */
20
11
  maxDelayMs?: number;
21
- /**
22
- * Upper bound for a wait the server asked for by `Retry-After` (default
23
- * 10000). Beyond it there is nothing to gain from holding a render open, so
24
- * the request fails immediately and carries `retryAfterMs` - the caller is in
25
- * a better position to decide between stale content and an error page.
26
- */
27
12
  maxRetryAfterMs?: number;
28
- /** HTTP statuses that trigger a retry (default [429, 503]). */
29
13
  retryStatuses?: number[];
30
14
  }
31
15
 
32
- /**
33
- * The cmssy cloud GraphQL delivery endpoint. It is the same for every workspace,
34
- * so consumers never need to set it - `apiUrl` defaults to this. Self-hosted /
35
- * staging deployments override it via config.
36
- */
37
16
  declare const DEFAULT_CMSSY_API_URL = "https://api.cmssy.io/graphql";
38
17
  declare function resolveApiUrl(apiUrl: string | undefined): string;
39
- /**
40
- * Public delivery endpoint for a workspace: the org-scoped path
41
- * `{base}/public/{orgSlug}/{workspaceSlug}/graphql`. `apiUrl` is the GraphQL
42
- * base (its trailing `/graphql` is stripped); the org path is what tells the
43
- * backend which workspace to serve, so slugs only need to be unique per org.
44
- */
45
- declare function resolvePublicUrl(config: CmssyClientConfig): string;
46
18
  interface FetchLikeResponse {
47
19
  ok: boolean;
48
20
  status: number;
@@ -66,11 +38,10 @@ interface FetchPageOptions {
66
38
  signal?: AbortSignal;
67
39
  retry?: RetryPolicy | false;
68
40
  }
69
- declare function normalizeSlug(path: string | string[] | undefined): string;
70
41
  declare function fetchPage(config: CmssyClientConfig, path: string | string[] | undefined, options?: FetchPageOptions): Promise<CmssyPageData | null>;
71
42
  declare function fetchPageById(config: CmssyClientConfig, pageId: string, options?: Pick<FetchPageOptions, "fetch" | "signal" | "retry">): Promise<CmssyPageData | null>;
72
43
  declare function fetchPages(config: CmssyClientConfig, options?: Pick<FetchPageOptions, "fetch" | "signal" | "retry">): Promise<CmssyPageSummary[]>;
73
44
  declare function fetchPageMeta(config: CmssyClientConfig, path: string | string[] | undefined, options?: Pick<FetchPageOptions, "fetch" | "signal" | "retry">): Promise<CmssyPageMeta | null>;
74
45
  declare function fetchLayouts(config: CmssyClientConfig, path: string | string[] | undefined, options?: FetchPageOptions): Promise<CmssyLayoutGroup[]>;
75
46
 
76
- export { CmssyRequestError as C, DEFAULT_CMSSY_API_URL as D, type FetchLike as F, type RetryPolicy as R, type FetchLikeResponse as a, type FetchPageOptions as b, fetchPage as c, fetchPageById as d, fetchPageMeta as e, fetchLayouts as f, fetchPages as g, resolvePublicUrl as h, normalizeSlug as n, resolveApiUrl as r };
47
+ export { CmssyRequestError as C, DEFAULT_CMSSY_API_URL as D, type FetchLike as F, type RetryPolicy as R, type FetchLikeResponse as a, type FetchPageOptions as b, fetchPage as c, fetchPageById as d, fetchPageMeta as e, fetchLayouts as f, fetchPages as g, resolveApiUrl as r };
@@ -1,48 +1,20 @@
1
1
  import { CmssyClientConfig, CmssyLayoutGroup, CmssyPageData, CmssyPageMeta, CmssyPageSummary } from '@cmssy/types';
2
2
 
3
- /** HTTP-level failure from the cmssy API, with a machine-readable status. */
4
3
  declare class CmssyRequestError extends Error {
5
4
  readonly status: number;
6
- /**
7
- * How long the server asked us to wait, in ms, when it said so. Present on a
8
- * 429 that carried `Retry-After`; a caller can serve stale content for that
9
- * long instead of guessing.
10
- */
11
5
  readonly retryAfterMs?: number;
12
6
  constructor(message: string, status: number, retryAfterMs?: number);
13
7
  }
14
8
  interface RetryPolicy {
15
- /** Additional attempts after the first request (default 3). */
16
9
  maxRetries?: number;
17
- /** Exponential backoff base in ms: base * 2^attempt (default 300). */
18
10
  baseDelayMs?: number;
19
- /** Upper bound for a guessed backoff wait (default 3000). */
20
11
  maxDelayMs?: number;
21
- /**
22
- * Upper bound for a wait the server asked for by `Retry-After` (default
23
- * 10000). Beyond it there is nothing to gain from holding a render open, so
24
- * the request fails immediately and carries `retryAfterMs` - the caller is in
25
- * a better position to decide between stale content and an error page.
26
- */
27
12
  maxRetryAfterMs?: number;
28
- /** HTTP statuses that trigger a retry (default [429, 503]). */
29
13
  retryStatuses?: number[];
30
14
  }
31
15
 
32
- /**
33
- * The cmssy cloud GraphQL delivery endpoint. It is the same for every workspace,
34
- * so consumers never need to set it - `apiUrl` defaults to this. Self-hosted /
35
- * staging deployments override it via config.
36
- */
37
16
  declare const DEFAULT_CMSSY_API_URL = "https://api.cmssy.io/graphql";
38
17
  declare function resolveApiUrl(apiUrl: string | undefined): string;
39
- /**
40
- * Public delivery endpoint for a workspace: the org-scoped path
41
- * `{base}/public/{orgSlug}/{workspaceSlug}/graphql`. `apiUrl` is the GraphQL
42
- * base (its trailing `/graphql` is stripped); the org path is what tells the
43
- * backend which workspace to serve, so slugs only need to be unique per org.
44
- */
45
- declare function resolvePublicUrl(config: CmssyClientConfig): string;
46
18
  interface FetchLikeResponse {
47
19
  ok: boolean;
48
20
  status: number;
@@ -66,11 +38,10 @@ interface FetchPageOptions {
66
38
  signal?: AbortSignal;
67
39
  retry?: RetryPolicy | false;
68
40
  }
69
- declare function normalizeSlug(path: string | string[] | undefined): string;
70
41
  declare function fetchPage(config: CmssyClientConfig, path: string | string[] | undefined, options?: FetchPageOptions): Promise<CmssyPageData | null>;
71
42
  declare function fetchPageById(config: CmssyClientConfig, pageId: string, options?: Pick<FetchPageOptions, "fetch" | "signal" | "retry">): Promise<CmssyPageData | null>;
72
43
  declare function fetchPages(config: CmssyClientConfig, options?: Pick<FetchPageOptions, "fetch" | "signal" | "retry">): Promise<CmssyPageSummary[]>;
73
44
  declare function fetchPageMeta(config: CmssyClientConfig, path: string | string[] | undefined, options?: Pick<FetchPageOptions, "fetch" | "signal" | "retry">): Promise<CmssyPageMeta | null>;
74
45
  declare function fetchLayouts(config: CmssyClientConfig, path: string | string[] | undefined, options?: FetchPageOptions): Promise<CmssyLayoutGroup[]>;
75
46
 
76
- export { CmssyRequestError as C, DEFAULT_CMSSY_API_URL as D, type FetchLike as F, type RetryPolicy as R, type FetchLikeResponse as a, type FetchPageOptions as b, fetchPage as c, fetchPageById as d, fetchPageMeta as e, fetchLayouts as f, fetchPages as g, resolvePublicUrl as h, normalizeSlug as n, resolveApiUrl as r };
47
+ export { CmssyRequestError as C, DEFAULT_CMSSY_API_URL as D, type FetchLike as F, type RetryPolicy as R, type FetchLikeResponse as a, type FetchPageOptions as b, fetchPage as c, fetchPageById as d, fetchPageMeta as e, fetchLayouts as f, fetchPages as g, resolveApiUrl as r };
@@ -1,82 +1,26 @@
1
1
  import { CmssyClientConfig, BlockRect, BlockSchema, BlockMeta } from '@cmssy/types';
2
- import { G as GraphqlRequestOptions } from './locale-Clouq4s4.js';
2
+ import { G as GraphqlRequestOptions } from './locale-DQZ80cew.js';
3
3
 
4
4
  declare const DEFAULT_CMSSY_EDITOR_ORIGINS: string[];
5
5
  declare function isDevelopment(): boolean;
6
6
  declare function resolveEditorOrigin(editorOrigin: string | string[] | undefined): string | string[];
7
7
  interface CmssyConfig {
8
- /**
9
- * Full GraphQL delivery endpoint. Defaults to the cmssy cloud endpoint
10
- * (`https://api.cmssy.io/graphql`); set it only for self-hosted / staging.
11
- */
12
8
  apiUrl?: string;
13
- /** Organization slug - part of the org-scoped delivery path. */
14
9
  org: string;
15
10
  workspaceSlug: string;
16
11
  draftSecret: string;
17
- /**
18
- * A cmssy API token (`cs_…`) that opts this app into the editor-controlled dev
19
- * preview. In development only, the SDK sends it on every page fetch so the
20
- * backend can resolve the token's user and honour that user's dev-preview flag
21
- * (toggled from the editor's dev-mode switch): flag on + a saved dev draft ⇒
22
- * the draft overlay renders, otherwise published content. Server-only (never
23
- * reaches the client); ignored outside development. See the Quickstart
24
- * "Dev preview" section.
25
- */
26
12
  devToken?: string;
27
- /**
28
- * Origin(s) allowed to frame your app in the editor: one origin, a list, or
29
- * the comma-separated string an env var carries - all three are accepted, so
30
- * `process.env.CMSSY_EDITOR_ORIGIN` can be passed through as-is. Defaults to
31
- * {@link DEFAULT_CMSSY_EDITOR_ORIGINS}; set it only for self-hosted admins.
32
- */
33
13
  editorOrigin?: string | string[];
34
- /**
35
- * Canonical absolute site URL (e.g. https://cmssy.com), used by
36
- * createCmssyRobots / createCmssySitemap. When omitted the helpers derive the
37
- * origin from the request `host` header at render time (multi-domain safe).
38
- */
39
14
  siteUrl?: string;
40
- /**
41
- * Fallback locale resolver for a site whose URLs carry no language (e.g. a
42
- * cookie or Accept-Language strategy). The workspace site config remains the
43
- * source of truth for the default and enabled languages.
44
- */
45
15
  resolveLocale?: () => string | Promise<string>;
46
16
  }
47
- /**
48
- * Env-shaped input for {@link defineCmssyConfig}: the required string fields are
49
- * widened to `string | undefined` so a config can pass `process.env.*` straight
50
- * through without a `?? ""` fallback (which would mask a missing value as an
51
- * empty string) or a cast.
52
- */
53
17
  type CmssyEnvConfig = Omit<CmssyConfig, "org" | "workspaceSlug" | "draftSecret"> & {
54
18
  org?: string;
55
19
  workspaceSlug?: string;
56
20
  draftSecret?: string;
57
21
  };
58
- /**
59
- * Validates an env-sourced config and returns a strictly-typed
60
- * {@link CmssyConfig}. Pass raw `process.env.*` values; this throws a clear,
61
- * actionable error listing any missing required variables (rendered by the
62
- * Next.js error overlay / boundary), so the app fails fast instead of running
63
- * with silently-empty config.
64
- */
65
22
  declare function defineCmssyConfig(config: CmssyEnvConfig): CmssyConfig;
66
23
 
67
- /**
68
- * Accepting a *typed* document, not a string, is what makes a query's variables
69
- * and its result checked together. graphql-codegen emits one of three shapes,
70
- * and the SDK takes all of them so an app never has to care which mode its
71
- * codegen runs in - or add `graphql` to its runtime dependencies to print an
72
- * AST it already has.
73
- */
74
- /**
75
- * A document that carries its result and variable types. Structurally
76
- * compatible with `TypedDocumentNode` (`@graphql-typed-document-node/core`) and
77
- * with graphql-codegen's `TypedDocumentString`, both of which declare the same
78
- * phantom `__apiType`.
79
- */
80
24
  interface CmssyTypedDocument<Result, Variables> {
81
25
  __apiType?: (variables: Variables) => Result;
82
26
  }
@@ -86,15 +30,8 @@ interface QueryScopedOptions extends GraphqlRequestOptions {
86
30
  }
87
31
  interface CmssyClient {
88
32
  readonly config: CmssyClientConfig;
89
- /**
90
- * Hand it a document that carries its types (what graphql-codegen emits, in
91
- * either mode) and the variables are checked and the result inferred; hand it
92
- * a query string and it behaves exactly as before. Same method, no second
93
- * way to do the same thing.
94
- */
95
33
  query<Result, Variables>(document: CmssyTypedDocument<Result, Variables>, variables: Variables, options?: GraphqlRequestOptions): Promise<Result>;
96
34
  query<T = unknown>(document: string, variables?: Record<string, unknown>, options?: GraphqlRequestOptions): Promise<T>;
97
- /** {@link CmssyClient.query} with the workspace id resolved and injected. */
98
35
  queryScoped<Result, Variables>(document: CmssyTypedDocument<Result, Variables>, variables: Omit<Variables, "workspaceId">, options?: QueryScopedOptions): Promise<Result>;
99
36
  queryScoped<T = unknown>(document: string, variables?: Record<string, unknown>, options?: QueryScopedOptions): Promise<T>;
100
37
  resolveWorkspaceId(options?: GraphqlRequestOptions): Promise<string>;
@@ -200,7 +137,6 @@ declare function resolveInitialTarget(editorOrigin: string | string[]): string;
200
137
  declare function parseEditorMessage(data: unknown, origin: string, expectedOrigin: string | string[]): EditorToAppMessage | null;
201
138
 
202
139
  interface CmssyCspOptions {
203
- /** Defaults to the cmssy cloud admin origin when unset. */
204
140
  editorOrigin?: string | string[];
205
141
  }
206
142
  interface MutableHeaders {
@@ -1,82 +1,26 @@
1
1
  import { CmssyClientConfig, BlockRect, BlockSchema, BlockMeta } from '@cmssy/types';
2
- import { G as GraphqlRequestOptions } from './locale-XMylB_kz.cjs';
2
+ import { G as GraphqlRequestOptions } from './locale-BS61R-fF.cjs';
3
3
 
4
4
  declare const DEFAULT_CMSSY_EDITOR_ORIGINS: string[];
5
5
  declare function isDevelopment(): boolean;
6
6
  declare function resolveEditorOrigin(editorOrigin: string | string[] | undefined): string | string[];
7
7
  interface CmssyConfig {
8
- /**
9
- * Full GraphQL delivery endpoint. Defaults to the cmssy cloud endpoint
10
- * (`https://api.cmssy.io/graphql`); set it only for self-hosted / staging.
11
- */
12
8
  apiUrl?: string;
13
- /** Organization slug - part of the org-scoped delivery path. */
14
9
  org: string;
15
10
  workspaceSlug: string;
16
11
  draftSecret: string;
17
- /**
18
- * A cmssy API token (`cs_…`) that opts this app into the editor-controlled dev
19
- * preview. In development only, the SDK sends it on every page fetch so the
20
- * backend can resolve the token's user and honour that user's dev-preview flag
21
- * (toggled from the editor's dev-mode switch): flag on + a saved dev draft ⇒
22
- * the draft overlay renders, otherwise published content. Server-only (never
23
- * reaches the client); ignored outside development. See the Quickstart
24
- * "Dev preview" section.
25
- */
26
12
  devToken?: string;
27
- /**
28
- * Origin(s) allowed to frame your app in the editor: one origin, a list, or
29
- * the comma-separated string an env var carries - all three are accepted, so
30
- * `process.env.CMSSY_EDITOR_ORIGIN` can be passed through as-is. Defaults to
31
- * {@link DEFAULT_CMSSY_EDITOR_ORIGINS}; set it only for self-hosted admins.
32
- */
33
13
  editorOrigin?: string | string[];
34
- /**
35
- * Canonical absolute site URL (e.g. https://cmssy.com), used by
36
- * createCmssyRobots / createCmssySitemap. When omitted the helpers derive the
37
- * origin from the request `host` header at render time (multi-domain safe).
38
- */
39
14
  siteUrl?: string;
40
- /**
41
- * Fallback locale resolver for a site whose URLs carry no language (e.g. a
42
- * cookie or Accept-Language strategy). The workspace site config remains the
43
- * source of truth for the default and enabled languages.
44
- */
45
15
  resolveLocale?: () => string | Promise<string>;
46
16
  }
47
- /**
48
- * Env-shaped input for {@link defineCmssyConfig}: the required string fields are
49
- * widened to `string | undefined` so a config can pass `process.env.*` straight
50
- * through without a `?? ""` fallback (which would mask a missing value as an
51
- * empty string) or a cast.
52
- */
53
17
  type CmssyEnvConfig = Omit<CmssyConfig, "org" | "workspaceSlug" | "draftSecret"> & {
54
18
  org?: string;
55
19
  workspaceSlug?: string;
56
20
  draftSecret?: string;
57
21
  };
58
- /**
59
- * Validates an env-sourced config and returns a strictly-typed
60
- * {@link CmssyConfig}. Pass raw `process.env.*` values; this throws a clear,
61
- * actionable error listing any missing required variables (rendered by the
62
- * Next.js error overlay / boundary), so the app fails fast instead of running
63
- * with silently-empty config.
64
- */
65
22
  declare function defineCmssyConfig(config: CmssyEnvConfig): CmssyConfig;
66
23
 
67
- /**
68
- * Accepting a *typed* document, not a string, is what makes a query's variables
69
- * and its result checked together. graphql-codegen emits one of three shapes,
70
- * and the SDK takes all of them so an app never has to care which mode its
71
- * codegen runs in - or add `graphql` to its runtime dependencies to print an
72
- * AST it already has.
73
- */
74
- /**
75
- * A document that carries its result and variable types. Structurally
76
- * compatible with `TypedDocumentNode` (`@graphql-typed-document-node/core`) and
77
- * with graphql-codegen's `TypedDocumentString`, both of which declare the same
78
- * phantom `__apiType`.
79
- */
80
24
  interface CmssyTypedDocument<Result, Variables> {
81
25
  __apiType?: (variables: Variables) => Result;
82
26
  }
@@ -86,15 +30,8 @@ interface QueryScopedOptions extends GraphqlRequestOptions {
86
30
  }
87
31
  interface CmssyClient {
88
32
  readonly config: CmssyClientConfig;
89
- /**
90
- * Hand it a document that carries its types (what graphql-codegen emits, in
91
- * either mode) and the variables are checked and the result inferred; hand it
92
- * a query string and it behaves exactly as before. Same method, no second
93
- * way to do the same thing.
94
- */
95
33
  query<Result, Variables>(document: CmssyTypedDocument<Result, Variables>, variables: Variables, options?: GraphqlRequestOptions): Promise<Result>;
96
34
  query<T = unknown>(document: string, variables?: Record<string, unknown>, options?: GraphqlRequestOptions): Promise<T>;
97
- /** {@link CmssyClient.query} with the workspace id resolved and injected. */
98
35
  queryScoped<Result, Variables>(document: CmssyTypedDocument<Result, Variables>, variables: Omit<Variables, "workspaceId">, options?: QueryScopedOptions): Promise<Result>;
99
36
  queryScoped<T = unknown>(document: string, variables?: Record<string, unknown>, options?: QueryScopedOptions): Promise<T>;
100
37
  resolveWorkspaceId(options?: GraphqlRequestOptions): Promise<string>;
@@ -200,7 +137,6 @@ declare function resolveInitialTarget(editorOrigin: string | string[]): string;
200
137
  declare function parseEditorMessage(data: unknown, origin: string, expectedOrigin: string | string[]): EditorToAppMessage | null;
201
138
 
202
139
  interface CmssyCspOptions {
203
- /** Defaults to the cmssy cloud admin origin when unset. */
204
140
  editorOrigin?: string | string[];
205
141
  }
206
142
  interface MutableHeaders {
package/dist/index.cjs CHANGED
@@ -76,11 +76,6 @@ Set the listed environment variables (e.g. in .env.local) and restart the dev se
76
76
  // src/data/http.ts
77
77
  var CmssyRequestError = class extends Error {
78
78
  status;
79
- /**
80
- * How long the server asked us to wait, in ms, when it said so. Present on a
81
- * 429 that carried `Retry-After`; a caller can serve stale content for that
82
- * long instead of guessing.
83
- */
84
79
  retryAfterMs;
85
80
  constructor(message, status, retryAfterMs2) {
86
81
  super(message);
@@ -451,8 +446,6 @@ function createCmssyClient(input) {
451
446
  const client = {
452
447
  config,
453
448
  resolveWorkspaceId: resolveWorkspaceId2,
454
- // One implementation behind both overloads: a string passes through
455
- // documentText untouched, a typed document gives up its query text.
456
449
  query: ((document2, variables = {}, options) => query(documentText(document2), variables, options)),
457
450
  queryScoped: ((document2, variables = {}, options = {}) => queryScoped(
458
451
  documentText(document2),
@@ -764,21 +757,24 @@ var CmssyWebhookError = class extends Error {
764
757
  }
765
758
  };
766
759
  var DEFAULT_TOLERANCE_SECONDS = 300;
760
+ var MAX_SIGNATURES = 8;
767
761
  function parseSignatureHeader(header) {
768
762
  let timestamp = null;
769
- let signature = null;
763
+ const signatures = [];
770
764
  for (const part of header.split(",")) {
771
765
  const idx = part.indexOf("=");
772
766
  if (idx === -1) continue;
773
767
  const key = part.slice(0, idx).trim();
774
768
  const value = part.slice(idx + 1).trim();
775
769
  if (key === "t") timestamp = Number(value);
776
- else if (key === "v1") signature = value;
770
+ else if (key === "v1" && value && signatures.length < MAX_SIGNATURES) {
771
+ signatures.push(value);
772
+ }
777
773
  }
778
- if (timestamp === null || !Number.isFinite(timestamp) || !signature) {
774
+ if (timestamp === null || !Number.isFinite(timestamp) || !signatures.length) {
779
775
  throw new CmssyWebhookError("Malformed X-Cmssy-Signature header");
780
776
  }
781
- return { timestamp, signature };
777
+ return { timestamp, signatures };
782
778
  }
783
779
  function hexToBytes(hex) {
784
780
  if (hex.length % 2 !== 0 || /[^0-9a-fA-F]/.test(hex)) return null;
@@ -792,9 +788,10 @@ function bytesToHex(bytes) {
792
788
  return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
793
789
  }
794
790
  function timingSafeHexEqual(expectedHex, providedHex) {
791
+ if (expectedHex.length !== providedHex.length) return false;
795
792
  const expected = hexToBytes(expectedHex);
796
793
  const provided = hexToBytes(providedHex);
797
- if (!expected || !provided || expected.length !== provided.length) {
794
+ if (!expected || !provided) {
798
795
  return false;
799
796
  }
800
797
  let diff = 0;
@@ -824,17 +821,28 @@ async function verifyCmssyWebhook(options) {
824
821
  if (!signatureHeader) {
825
822
  throw new CmssyWebhookError("Missing X-Cmssy-Signature header");
826
823
  }
827
- if (!secret) {
824
+ const candidates = typeof secret === "string" ? [secret] : Array.isArray(secret) ? secret : [];
825
+ if (candidates.some((value) => typeof value !== "string")) {
826
+ throw new CmssyWebhookError("Webhook secret must be a string");
827
+ }
828
+ const secrets = candidates.filter(Boolean);
829
+ if (!secrets.length) {
828
830
  throw new CmssyWebhookError("Missing webhook secret");
829
831
  }
830
- const { timestamp, signature } = parseSignatureHeader(signatureHeader);
832
+ const { timestamp, signatures } = parseSignatureHeader(signatureHeader);
831
833
  const toleranceMs = (options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS) * 1e3;
832
834
  const now = options.now ?? Date.now();
833
835
  if (Math.abs(now - timestamp) > toleranceMs) {
834
836
  throw new CmssyWebhookError("Webhook timestamp outside tolerance");
835
837
  }
836
- const expected = await hmacSha256Hex(secret, `${timestamp}.${body}`);
837
- if (!timingSafeHexEqual(expected, signature)) {
838
+ let matched = false;
839
+ for (const candidate of secrets) {
840
+ const expected = await hmacSha256Hex(candidate, `${timestamp}.${body}`);
841
+ for (const signature of signatures) {
842
+ if (timingSafeHexEqual(expected, signature)) matched = true;
843
+ }
844
+ }
845
+ if (!matched) {
838
846
  throw new CmssyWebhookError("Webhook signature mismatch");
839
847
  }
840
848
  let parsed;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- export { A as AppToEditorMessage, B as BoundsMessage, C as ClickMessage, a as CmssyClient, b as CmssyConfig, c as CmssyCspOptions, d as CmssyEnvConfig, e as CmssyTypedDocument, D as DEFAULT_CMSSY_EDITOR_ORIGINS, E as EditorToAppMessage, P as PROTOCOL_VERSION, f as ParentReadyMessage, g as PatchMessage, h as PostTarget, Q as QueryScopedOptions, R as ReadyMessage, S as SelectMessage, i as applyCmssyCsp, j as createCmssyClient, k as defineCmssyConfig, l as isProtocolCompatible, n as normalizeOrigin, p as parseEditorMessage, m as postToEditor, r as resolveEditorOrigin } from './csp-0D9pcuDp.cjs';
2
- export { C as CMSSY_LOCALE_HEADER, G as GraphqlRequestOptions, g as graphqlRequest, l as localizeHref, r as resolveCmssyLocale } from './locale-XMylB_kz.cjs';
3
- export { C as CmssyRequestError, D as DEFAULT_CMSSY_API_URL, F as FetchLike, a as FetchLikeResponse, b as FetchPageOptions, R as RetryPolicy } from './content-client-CJQ-nafm.cjs';
1
+ export { A as AppToEditorMessage, B as BoundsMessage, C as ClickMessage, a as CmssyClient, b as CmssyConfig, c as CmssyCspOptions, d as CmssyEnvConfig, e as CmssyTypedDocument, D as DEFAULT_CMSSY_EDITOR_ORIGINS, E as EditorToAppMessage, P as PROTOCOL_VERSION, f as ParentReadyMessage, g as PatchMessage, h as PostTarget, Q as QueryScopedOptions, R as ReadyMessage, S as SelectMessage, i as applyCmssyCsp, j as createCmssyClient, k as defineCmssyConfig, l as isProtocolCompatible, n as normalizeOrigin, p as parseEditorMessage, m as postToEditor, r as resolveEditorOrigin } from './csp-ZchQ-sC7.cjs';
2
+ export { C as CMSSY_LOCALE_HEADER, G as GraphqlRequestOptions, g as graphqlRequest, l as localizeHref, r as resolveCmssyLocale } from './locale-BS61R-fF.cjs';
3
+ export { C as CmssyRequestError, D as DEFAULT_CMSSY_API_URL, F as FetchLike, a as FetchLikeResponse, b as FetchPageOptions, R as RetryPolicy } from './content-client-BPpRfq0h.cjs';
4
4
  import * as _cmssy_types from '@cmssy/types';
5
5
  import { FieldOptions, TypedField, BlockPropsSchema, InferBlockContent, RelationMode, CmssyModelRecord, VerifyCmssyWebhookOptions, CmssyWebhookEvent } from '@cmssy/types';
6
6
  export { BlockMeta, BlockPropsSchema, BlockRect, BlockSchema, BuildBlockContextExtra, CmssyBlockAuthContext, CmssyBlockContext, CmssyBlockMember, CmssyBlockPage, CmssyBlockWorkspace, CmssyBranding, CmssyClientConfig, CmssyFormDefinition, CmssyFormField, CmssyFormSettings, CmssyFormSubmitResponse, CmssyLayoutGroup, CmssyLayoutSettings, CmssyLocaleContext, CmssyLocalizedValue, CmssyModelDefinition, CmssyModelRecord, CmssyPageData, CmssyPageMeta, CmssyPageSummary, CmssyRecordList, CmssySiteConfig, CmssyWebhookEvent, CmssyWebhookOrder, FieldCondition, FieldConditionGroup, FieldConditionLogic, FieldControl, FieldDefinition, FieldOptions, FieldType, InferBlockContent, LayoutPosition, RawBlock, RawLayoutBlock, SubmitFormInput, TypedField, VerifyCmssyWebhookOptions, evaluateFieldConditionGroup, layoutPositionValues } from '@cmssy/types';
@@ -17,25 +17,13 @@ type MediaValue<O> = O extends {
17
17
  type RepeaterValue<O> = O extends {
18
18
  itemSchema: infer Schema extends BlockPropsSchema;
19
19
  } ? InferBlockContent<Schema>[] : Record<string, unknown>[];
20
- /**
21
- * The value a relation field holds AFTER server-side resolution: the SDK
22
- * replaces the stored record id(s) with the records themselves before the
23
- * component renders. `mode: "all"` and `multiple: true` yield a list; the
24
- * default is a single record - `undefined` when the reference dangles, which
25
- * no `required` flag can rule out.
26
- */
27
20
  type RelationValue<O> = O extends {
28
21
  mode: "all";
29
22
  } | {
30
23
  multiple: true;
31
24
  } ? CmssyModelRecord[] : CmssyModelRecord | undefined;
32
25
  interface RelationFieldOptions extends Omit<FieldOptions, "options" | "itemSchema"> {
33
- /** Slug of the model the field points at. */
34
26
  model: string;
35
- /**
36
- * "all" binds the field to every record of the model (no editor picking;
37
- * `sort`/`limit` shape the list). Omit it for editor-picked record id(s).
38
- */
39
27
  mode?: RelationMode;
40
28
  multiple?: boolean;
41
29
  sort?: string;
@@ -79,26 +67,9 @@ declare function isVerifiedEditUrl(url: {
79
67
  draftSecret: string;
80
68
  }): Promise<boolean>;
81
69
 
82
- /**
83
- * Verify + parse an inbound cmssy webhook (CMS-693 / CMS-694).
84
- *
85
- * cmssy signs each delivery with HMAC-SHA256 over `${timestamp}.${body}`
86
- * and sends the result in the `X-Cmssy-Signature: t=<ms>,v1=<hex>`
87
- * header, plus a unique `X-Cmssy-Webhook-Id`. This helper recomputes the
88
- * signature (timing-safe compare), rejects stale timestamps to bound
89
- * replay, and returns the typed event.
90
- *
91
- * IMPORTANT: pass the RAW request body string (e.g. `await req.text()`),
92
- * never a re-serialized object - the signed bytes must match exactly.
93
- */
94
70
  declare class CmssyWebhookError extends Error {
95
71
  constructor(message: string);
96
72
  }
97
- /**
98
- * Verify the signature + freshness and return the parsed event. Throws
99
- * `CmssyWebhookError` on any failure (missing/malformed header, bad
100
- * signature, stale timestamp, invalid JSON) - catch it and respond 400.
101
- */
102
73
  declare function verifyCmssyWebhook(options: VerifyCmssyWebhookOptions): Promise<CmssyWebhookEvent>;
103
74
 
104
75
  export { CMSSY_EDIT_HEADER, CMSSY_EDIT_QUERY_PARAM, CMSSY_SECRET_QUERY_PARAM, CmssyWebhookError, fields, isVerifiedEditUrl, verifyCmssyWebhook };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { A as AppToEditorMessage, B as BoundsMessage, C as ClickMessage, a as CmssyClient, b as CmssyConfig, c as CmssyCspOptions, d as CmssyEnvConfig, e as CmssyTypedDocument, D as DEFAULT_CMSSY_EDITOR_ORIGINS, E as EditorToAppMessage, P as PROTOCOL_VERSION, f as ParentReadyMessage, g as PatchMessage, h as PostTarget, Q as QueryScopedOptions, R as ReadyMessage, S as SelectMessage, i as applyCmssyCsp, j as createCmssyClient, k as defineCmssyConfig, l as isProtocolCompatible, n as normalizeOrigin, p as parseEditorMessage, m as postToEditor, r as resolveEditorOrigin } from './csp-CpiqfM03.js';
2
- export { C as CMSSY_LOCALE_HEADER, G as GraphqlRequestOptions, g as graphqlRequest, l as localizeHref, r as resolveCmssyLocale } from './locale-Clouq4s4.js';
3
- export { C as CmssyRequestError, D as DEFAULT_CMSSY_API_URL, F as FetchLike, a as FetchLikeResponse, b as FetchPageOptions, R as RetryPolicy } from './content-client-CJQ-nafm.js';
1
+ export { A as AppToEditorMessage, B as BoundsMessage, C as ClickMessage, a as CmssyClient, b as CmssyConfig, c as CmssyCspOptions, d as CmssyEnvConfig, e as CmssyTypedDocument, D as DEFAULT_CMSSY_EDITOR_ORIGINS, E as EditorToAppMessage, P as PROTOCOL_VERSION, f as ParentReadyMessage, g as PatchMessage, h as PostTarget, Q as QueryScopedOptions, R as ReadyMessage, S as SelectMessage, i as applyCmssyCsp, j as createCmssyClient, k as defineCmssyConfig, l as isProtocolCompatible, n as normalizeOrigin, p as parseEditorMessage, m as postToEditor, r as resolveEditorOrigin } from './csp-DjFT5SmH.js';
2
+ export { C as CMSSY_LOCALE_HEADER, G as GraphqlRequestOptions, g as graphqlRequest, l as localizeHref, r as resolveCmssyLocale } from './locale-DQZ80cew.js';
3
+ export { C as CmssyRequestError, D as DEFAULT_CMSSY_API_URL, F as FetchLike, a as FetchLikeResponse, b as FetchPageOptions, R as RetryPolicy } from './content-client-BPpRfq0h.js';
4
4
  import * as _cmssy_types from '@cmssy/types';
5
5
  import { FieldOptions, TypedField, BlockPropsSchema, InferBlockContent, RelationMode, CmssyModelRecord, VerifyCmssyWebhookOptions, CmssyWebhookEvent } from '@cmssy/types';
6
6
  export { BlockMeta, BlockPropsSchema, BlockRect, BlockSchema, BuildBlockContextExtra, CmssyBlockAuthContext, CmssyBlockContext, CmssyBlockMember, CmssyBlockPage, CmssyBlockWorkspace, CmssyBranding, CmssyClientConfig, CmssyFormDefinition, CmssyFormField, CmssyFormSettings, CmssyFormSubmitResponse, CmssyLayoutGroup, CmssyLayoutSettings, CmssyLocaleContext, CmssyLocalizedValue, CmssyModelDefinition, CmssyModelRecord, CmssyPageData, CmssyPageMeta, CmssyPageSummary, CmssyRecordList, CmssySiteConfig, CmssyWebhookEvent, CmssyWebhookOrder, FieldCondition, FieldConditionGroup, FieldConditionLogic, FieldControl, FieldDefinition, FieldOptions, FieldType, InferBlockContent, LayoutPosition, RawBlock, RawLayoutBlock, SubmitFormInput, TypedField, VerifyCmssyWebhookOptions, evaluateFieldConditionGroup, layoutPositionValues } from '@cmssy/types';
@@ -17,25 +17,13 @@ type MediaValue<O> = O extends {
17
17
  type RepeaterValue<O> = O extends {
18
18
  itemSchema: infer Schema extends BlockPropsSchema;
19
19
  } ? InferBlockContent<Schema>[] : Record<string, unknown>[];
20
- /**
21
- * The value a relation field holds AFTER server-side resolution: the SDK
22
- * replaces the stored record id(s) with the records themselves before the
23
- * component renders. `mode: "all"` and `multiple: true` yield a list; the
24
- * default is a single record - `undefined` when the reference dangles, which
25
- * no `required` flag can rule out.
26
- */
27
20
  type RelationValue<O> = O extends {
28
21
  mode: "all";
29
22
  } | {
30
23
  multiple: true;
31
24
  } ? CmssyModelRecord[] : CmssyModelRecord | undefined;
32
25
  interface RelationFieldOptions extends Omit<FieldOptions, "options" | "itemSchema"> {
33
- /** Slug of the model the field points at. */
34
26
  model: string;
35
- /**
36
- * "all" binds the field to every record of the model (no editor picking;
37
- * `sort`/`limit` shape the list). Omit it for editor-picked record id(s).
38
- */
39
27
  mode?: RelationMode;
40
28
  multiple?: boolean;
41
29
  sort?: string;
@@ -79,26 +67,9 @@ declare function isVerifiedEditUrl(url: {
79
67
  draftSecret: string;
80
68
  }): Promise<boolean>;
81
69
 
82
- /**
83
- * Verify + parse an inbound cmssy webhook (CMS-693 / CMS-694).
84
- *
85
- * cmssy signs each delivery with HMAC-SHA256 over `${timestamp}.${body}`
86
- * and sends the result in the `X-Cmssy-Signature: t=<ms>,v1=<hex>`
87
- * header, plus a unique `X-Cmssy-Webhook-Id`. This helper recomputes the
88
- * signature (timing-safe compare), rejects stale timestamps to bound
89
- * replay, and returns the typed event.
90
- *
91
- * IMPORTANT: pass the RAW request body string (e.g. `await req.text()`),
92
- * never a re-serialized object - the signed bytes must match exactly.
93
- */
94
70
  declare class CmssyWebhookError extends Error {
95
71
  constructor(message: string);
96
72
  }
97
- /**
98
- * Verify the signature + freshness and return the parsed event. Throws
99
- * `CmssyWebhookError` on any failure (missing/malformed header, bad
100
- * signature, stale timestamp, invalid JSON) - catch it and respond 400.
101
- */
102
73
  declare function verifyCmssyWebhook(options: VerifyCmssyWebhookOptions): Promise<CmssyWebhookEvent>;
103
74
 
104
75
  export { CMSSY_EDIT_HEADER, CMSSY_EDIT_QUERY_PARAM, CMSSY_SECRET_QUERY_PARAM, CmssyWebhookError, fields, isVerifiedEditUrl, verifyCmssyWebhook };
package/dist/index.js CHANGED
@@ -74,11 +74,6 @@ Set the listed environment variables (e.g. in .env.local) and restart the dev se
74
74
  // src/data/http.ts
75
75
  var CmssyRequestError = class extends Error {
76
76
  status;
77
- /**
78
- * How long the server asked us to wait, in ms, when it said so. Present on a
79
- * 429 that carried `Retry-After`; a caller can serve stale content for that
80
- * long instead of guessing.
81
- */
82
77
  retryAfterMs;
83
78
  constructor(message, status, retryAfterMs2) {
84
79
  super(message);
@@ -449,8 +444,6 @@ function createCmssyClient(input) {
449
444
  const client = {
450
445
  config,
451
446
  resolveWorkspaceId: resolveWorkspaceId2,
452
- // One implementation behind both overloads: a string passes through
453
- // documentText untouched, a typed document gives up its query text.
454
447
  query: ((document2, variables = {}, options) => query(documentText(document2), variables, options)),
455
448
  queryScoped: ((document2, variables = {}, options = {}) => queryScoped(
456
449
  documentText(document2),
@@ -762,21 +755,24 @@ var CmssyWebhookError = class extends Error {
762
755
  }
763
756
  };
764
757
  var DEFAULT_TOLERANCE_SECONDS = 300;
758
+ var MAX_SIGNATURES = 8;
765
759
  function parseSignatureHeader(header) {
766
760
  let timestamp = null;
767
- let signature = null;
761
+ const signatures = [];
768
762
  for (const part of header.split(",")) {
769
763
  const idx = part.indexOf("=");
770
764
  if (idx === -1) continue;
771
765
  const key = part.slice(0, idx).trim();
772
766
  const value = part.slice(idx + 1).trim();
773
767
  if (key === "t") timestamp = Number(value);
774
- else if (key === "v1") signature = value;
768
+ else if (key === "v1" && value && signatures.length < MAX_SIGNATURES) {
769
+ signatures.push(value);
770
+ }
775
771
  }
776
- if (timestamp === null || !Number.isFinite(timestamp) || !signature) {
772
+ if (timestamp === null || !Number.isFinite(timestamp) || !signatures.length) {
777
773
  throw new CmssyWebhookError("Malformed X-Cmssy-Signature header");
778
774
  }
779
- return { timestamp, signature };
775
+ return { timestamp, signatures };
780
776
  }
781
777
  function hexToBytes(hex) {
782
778
  if (hex.length % 2 !== 0 || /[^0-9a-fA-F]/.test(hex)) return null;
@@ -790,9 +786,10 @@ function bytesToHex(bytes) {
790
786
  return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
791
787
  }
792
788
  function timingSafeHexEqual(expectedHex, providedHex) {
789
+ if (expectedHex.length !== providedHex.length) return false;
793
790
  const expected = hexToBytes(expectedHex);
794
791
  const provided = hexToBytes(providedHex);
795
- if (!expected || !provided || expected.length !== provided.length) {
792
+ if (!expected || !provided) {
796
793
  return false;
797
794
  }
798
795
  let diff = 0;
@@ -822,17 +819,28 @@ async function verifyCmssyWebhook(options) {
822
819
  if (!signatureHeader) {
823
820
  throw new CmssyWebhookError("Missing X-Cmssy-Signature header");
824
821
  }
825
- if (!secret) {
822
+ const candidates = typeof secret === "string" ? [secret] : Array.isArray(secret) ? secret : [];
823
+ if (candidates.some((value) => typeof value !== "string")) {
824
+ throw new CmssyWebhookError("Webhook secret must be a string");
825
+ }
826
+ const secrets = candidates.filter(Boolean);
827
+ if (!secrets.length) {
826
828
  throw new CmssyWebhookError("Missing webhook secret");
827
829
  }
828
- const { timestamp, signature } = parseSignatureHeader(signatureHeader);
830
+ const { timestamp, signatures } = parseSignatureHeader(signatureHeader);
829
831
  const toleranceMs = (options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS) * 1e3;
830
832
  const now = options.now ?? Date.now();
831
833
  if (Math.abs(now - timestamp) > toleranceMs) {
832
834
  throw new CmssyWebhookError("Webhook timestamp outside tolerance");
833
835
  }
834
- const expected = await hmacSha256Hex(secret, `${timestamp}.${body}`);
835
- if (!timingSafeHexEqual(expected, signature)) {
836
+ let matched = false;
837
+ for (const candidate of secrets) {
838
+ const expected = await hmacSha256Hex(candidate, `${timestamp}.${body}`);
839
+ for (const signature of signatures) {
840
+ if (timingSafeHexEqual(expected, signature)) matched = true;
841
+ }
842
+ }
843
+ if (!matched) {
836
844
  throw new CmssyWebhookError("Webhook signature mismatch");
837
845
  }
838
846
  let parsed;