@cloudcannon/sdk 0.0.4 → 0.0.6

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
@@ -30,29 +30,49 @@ npm install @cloudcannon/sdk
30
30
 
31
31
  ## Creating and configuring the client
32
32
 
33
- Import `CloudCannonClient` and instantiate it with your API key:
33
+ Import `CloudCannonClient` and instantiate it with your API key or user access key:
34
34
 
35
35
  ```typescript
36
36
  import CloudCannonClient from '@cloudcannon/sdk';
37
37
 
38
+ // Authenticate with an API key
38
39
  const client = new CloudCannonClient({
39
40
  key: 'your-api-key-here',
40
41
  });
42
+
43
+ // Authenticate with a user access key
44
+ const client = new CloudCannonClient({
45
+ userAccessKey: {
46
+ id: 'your-access-key-id',
47
+ secret: 'your-access-key-secret',
48
+ },
49
+ });
41
50
  ```
42
51
 
43
52
  ### Optional configuration
44
53
 
45
54
  | Option | Type | Description |
46
55
  |--------|------|-------------|
47
- | `key` | `string` | **Required.** Your CloudCannon API key. |
56
+ | `key` | `string` | Your CloudCannon API key. Required if `userAccessKey` is not provided. |
57
+ | `userAccessKey` | `{ id: string, secret: string }` | User access key for HMAC-SHA256 request signing. Required if `key` is not provided. |
48
58
  | `apiOrigin` | `string` | The API host. Defaults to `app.cloudcannon.com`. |
49
- | `getCustomAuthHeaders` | `() => Record<string, string>` | Provide custom authentication headers instead of the default `X-API-KEY`. |
59
+ | `getCustomAuthHeaders` | `() => Record<string, string>` | Provide custom authentication headers instead of the default auth headers. |
50
60
 
51
61
  ```typescript
62
+ // API key authentication
52
63
  const client = new CloudCannonClient({
53
64
  key: 'your-api-key-here',
54
65
  apiOrigin: 'app.cloudcannon.com',
55
66
  });
67
+
68
+ // User access key authentication
69
+ const client = new CloudCannonClient({
70
+ userAccessKey: {
71
+ id: 'your-access-key-id',
72
+ secret: 'your-access-key-secret',
73
+ },
74
+ apiOrigin: 'app.cloudcannon.com',
75
+ });
56
76
  ```
57
77
 
58
78
  ## Client functions
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ import { type ConnectSiteOptions, type CreateDamOptions, type CreateInboxOptions
9
9
  import { type BuildConfiguration, type ConnectDamOptions, type ConnectInboxOptions, type CopySiteOptions, type CreateBackupOptions, type ListSiteBackupsOptions, type ListSiteBuildsOptions, type ListSiteSyncsOptions, SiteClient, type UpdateSiteOptions, type UploadFileOptions } from './src/site.ts';
10
10
  import { SiteInboxClient, type UpdateInboxOptions } from './src/site-inbox.ts';
11
11
  import { SyncClient } from './src/sync.ts';
12
+ export { ApiError, AuthenticationError, } from './src/errors.ts';
12
13
  export type { BuildConfiguration, CommitEditingSessionOptions, CommitEditingSessionResponse, ConnectDamOptions, ConnectInboxOptions, ConnectSiteOptions, CopySiteOptions, CreateBackupOptions, CreateContributionOptions, CreateDamOptions, CreateEditingSessionFileOptions, CreateInboxOptions, FilterOptions, ListInboxSubmissionsOptions, ListOrgDamsOptions, ListOrgInboxesOptions, ListOrgSitesOptions, ListOrgsOptions, ListSiteBackupsOptions, ListSiteBuildsOptions, ListSiteSyncsOptions, PaginatedResponse, PaginationOptions, SortingOptions, UnlockOptions, UpdateInboxOptions, UpdateSiteOptions, UploadFileOptions, };
13
14
  export type Provider = operations['Providers_Repositories']['parameters']['path']['provider'];
14
15
  export type Site = components['schemas']['SiteBlueprint'];
@@ -72,15 +73,24 @@ type MatchURL<M extends keyof paths[keyof paths], U extends string> = {
72
73
  type ValidURL<M extends keyof paths[keyof paths], U extends string> = MatchURL<M, U> extends never ? {
73
74
  [K in keyof paths]: RequestParams<paths[K][M]> extends never ? never : StripPrefix<K>;
74
75
  }[keyof paths] : U;
75
- export type CloudCannonClientConfig = {
76
- key: string;
76
+ type BaseCloudCannonClientConfig = {
77
77
  apiOrigin?: string;
78
78
  getCustomAuthHeaders?: () => Record<string, string>;
79
79
  };
80
+ export type CloudCannonClientConfig = ({
81
+ key: string;
82
+ } & BaseCloudCannonClientConfig) | ({
83
+ userAccessKey: UserAccessKey;
84
+ } & BaseCloudCannonClientConfig);
85
+ export type UserAccessKey = {
86
+ id: string;
87
+ secret: string;
88
+ };
80
89
  export default class CloudCannonClient {
81
90
  #private;
82
91
  constructor(config: CloudCannonClientConfig);
83
- getAuthHeaders(): Record<string, string>;
92
+ getAuthHeaders(url: string, body?: string): Promise<Record<string, string>>;
93
+ signRequest(userAccessKey: UserAccessKey, url: string, body?: string | null): Promise<Record<string, string>>;
84
94
  fetch<const U extends string, const M extends Uppercase<keyof paths[keyof paths]> = 'GET'>(url: ValidURL<Lowercase<M>, U>, options?: Omit<RequestInit, keyof RequestMixin<M, MatchURL<Lowercase<M>, U>[Lowercase<M>]>> & RequestMixin<M, MatchURL<Lowercase<M>, U>[Lowercase<M>]>): Promise<APIResponse<MatchURL<Lowercase<M>, U>[Lowercase<M>]>>;
85
95
  org(uuid: string): OrgClient;
86
96
  site(uuid: string): SiteClient;
package/dist/index.js CHANGED
@@ -1,50 +1,82 @@
1
+ import { createHmac, randomUUID } from 'node:crypto';
1
2
  import { BackupClient } from "./src/backup.js";
2
3
  import { BuildClient } from "./src/build.js";
3
4
  import { EditingSessionClient, } from "./src/editing-session.js";
4
5
  import { EditingSessionFileClient, } from "./src/editing-session-file.js";
6
+ import { AuthenticationError } from "./src/errors.js";
5
7
  import { buildQuery, paginatedResponse, } from "./src/helpers/query.js";
6
8
  import { InboxClient } from "./src/inbox.js";
7
9
  import { OrgClient, } from "./src/org.js";
8
10
  import { SiteClient, } from "./src/site.js";
9
11
  import { SiteInboxClient } from "./src/site-inbox.js";
10
12
  import { SyncClient } from "./src/sync.js";
13
+ export { ApiError, AuthenticationError, } from "./src/errors.js";
11
14
  export default class CloudCannonClient {
12
15
  #apiKey;
16
+ #userAccessKey;
13
17
  #appDomain;
14
18
  #getCustomAuthHeaders;
15
19
  constructor(config) {
16
- this.#apiKey = config.key;
20
+ if ('userAccessKey' in config) {
21
+ this.#userAccessKey = config.userAccessKey;
22
+ }
23
+ else {
24
+ this.#apiKey = config.key;
25
+ }
17
26
  this.#appDomain = config.apiOrigin ?? 'app.cloudcannon.com';
18
27
  this.#getCustomAuthHeaders = config.getCustomAuthHeaders;
19
28
  }
20
- getAuthHeaders() {
29
+ async getAuthHeaders(url, body) {
21
30
  if (this.#getCustomAuthHeaders) {
22
- return {
23
- ...this.#getCustomAuthHeaders(),
24
- };
31
+ return this.#getCustomAuthHeaders();
32
+ }
33
+ if (this.#userAccessKey) {
34
+ return this.signRequest(this.#userAccessKey, url, body);
25
35
  }
26
36
  return {
27
37
  'X-API-KEY': `${this.#apiKey}`,
28
38
  };
29
39
  }
40
+ async signRequest(userAccessKey, url, body) {
41
+ const signedAtISO = new Date().toISOString();
42
+ const nonce = randomUUID();
43
+ const key = Buffer.from(userAccessKey.secret, 'base64');
44
+ const message = JSON.stringify({ url, signed_at: signedAtISO, body: body ?? '', nonce });
45
+ const digest = createHmac('sha256', key).update(message).digest('hex');
46
+ return {
47
+ 'X-CC-ACCESS-KEY': userAccessKey.id,
48
+ 'X-CC-SIGNED-AT': signedAtISO,
49
+ 'X-CC-CHECKSUM': digest,
50
+ 'X-CC-NONCE': nonce,
51
+ };
52
+ }
30
53
  async fetch(url, options) {
54
+ const fullUrl = `https://${this.#appDomain}/api/v0${url}`;
55
+ let body;
56
+ if (options?.body) {
57
+ if (typeof options?.body !== 'string') {
58
+ body = JSON.stringify(options.body);
59
+ }
60
+ else {
61
+ body = options.body;
62
+ }
63
+ }
64
+ const authHeaders = await this.getAuthHeaders(fullUrl, body);
31
65
  const requestInit = {
32
66
  ...options,
33
67
  headers: {
34
- ...this.getAuthHeaders(),
68
+ ...authHeaders,
35
69
  'Content-Type': 'application/json',
70
+ 'X-Requested-With': 'XMLHttpRequest',
36
71
  ...options?.headers,
37
72
  },
73
+ body,
38
74
  };
39
- if (options?.body) {
40
- if (typeof options?.body !== 'string') {
41
- requestInit.body = JSON.stringify(options.body);
42
- }
43
- else {
44
- requestInit.body = options.body;
45
- }
75
+ const resp = (await fetch(fullUrl, requestInit));
76
+ if (resp.status === 401) {
77
+ throw new AuthenticationError('Failed to authenticate with the CloudCannon API.', fullUrl, options);
46
78
  }
47
- return fetch(`https://${this.#appDomain}/api/v0${url}`, requestInit);
79
+ return resp;
48
80
  }
49
81
  org(uuid) {
50
82
  return new OrgClient(uuid, this);
@@ -75,7 +107,7 @@ export default class CloudCannonClient {
75
107
  }
76
108
  async orgs(options = {}) {
77
109
  const query = buildQuery(options);
78
- const resp = await this.fetch(`/orgs?${query}`);
110
+ const resp = await this.fetch(`/orgs${query}`);
79
111
  if (resp.status === 403) {
80
112
  throw new Error('Error fetching orgs. Permission denied');
81
113
  }
@@ -5,3 +5,6 @@ export declare class ApiError extends Error {
5
5
  status: number | null;
6
6
  constructor(message: string, errors: unknown, url: string, options: unknown, status: number | null);
7
7
  }
8
+ export declare class AuthenticationError extends ApiError {
9
+ constructor(message: string, url: string, options: unknown);
10
+ }
@@ -11,3 +11,8 @@ export class ApiError extends Error {
11
11
  this.status = status;
12
12
  }
13
13
  }
14
+ export class AuthenticationError extends ApiError {
15
+ constructor(message, url, options) {
16
+ super(message, undefined, url, options, 401);
17
+ }
18
+ }
@@ -24,5 +24,5 @@ export declare function buildQuery(options?: PaginationOptions & {
24
24
  sort_attribute?: string;
25
25
  sort_direction?: 'ASC' | 'DESC';
26
26
  filters?: Record<string, unknown>;
27
- }): string;
27
+ }): `?${string}` | '';
28
28
  export declare function paginatedResponse<T>(items: T[], headers: Headers): PaginatedResponse<T>;
@@ -15,7 +15,11 @@ export function buildQuery(options = {}) {
15
15
  }
16
16
  }
17
17
  }
18
- return params.toString();
18
+ const result = params.toString();
19
+ if (result.length > 0) {
20
+ return `?${result}`;
21
+ }
22
+ return '';
19
23
  }
20
24
  export function paginatedResponse(items, headers) {
21
25
  const parse = (name) => {
package/dist/src/inbox.js CHANGED
@@ -8,7 +8,7 @@ export class InboxClient {
8
8
  }
9
9
  async getSubmissions(options = {}) {
10
10
  const query = buildQuery(options);
11
- const resp = await this.#client.fetch(`/inboxes/${this.#uuid}/form-hooks?${query}`);
11
+ const resp = await this.#client.fetch(`/inboxes/${this.#uuid}/form-hooks${query}`);
12
12
  if (resp.status === 401 || resp.status === 403) {
13
13
  throw new Error('Error fetching submissions. Permission denied');
14
14
  }
package/dist/src/org.js CHANGED
@@ -9,7 +9,7 @@ export class OrgClient {
9
9
  }
10
10
  async sites(options = {}) {
11
11
  const query = buildQuery(options);
12
- const resp = await this.#client.fetch(`/orgs/${this.#uuid}/sites?${query}`);
12
+ const resp = await this.#client.fetch(`/orgs/${this.#uuid}/sites${query}`);
13
13
  if (resp.status === 401 || resp.status === 403) {
14
14
  throw new Error('Error fetching sites. Permission denied');
15
15
  }
@@ -67,7 +67,7 @@ export class OrgClient {
67
67
  }
68
68
  async getInboxes(options = {}) {
69
69
  const query = buildQuery(options);
70
- const resp = await this.#client.fetch(`/orgs/${this.#uuid}/inboxes?${query}`);
70
+ const resp = await this.#client.fetch(`/orgs/${this.#uuid}/inboxes${query}`);
71
71
  if (resp.status === 403) {
72
72
  throw new Error('Error fetching inboxes. Permission denied');
73
73
  }
@@ -91,7 +91,7 @@ export class OrgClient {
91
91
  }
92
92
  async getDams(options = {}) {
93
93
  const query = buildQuery(options);
94
- const resp = await this.#client.fetch(`/orgs/${this.#uuid}/dams?${query}`);
94
+ const resp = await this.#client.fetch(`/orgs/${this.#uuid}/dams${query}`);
95
95
  const dams = await resp.json();
96
96
  return paginatedResponse(dams, resp.headers);
97
97
  }
package/dist/src/site.js CHANGED
@@ -73,7 +73,7 @@ export class SiteClient {
73
73
  }
74
74
  async getBuilds(options = {}) {
75
75
  const query = buildQuery(options);
76
- const resp = await this.#client.fetch(`/sites/${this.#uuid}/builds?${query}`);
76
+ const resp = await this.#client.fetch(`/sites/${this.#uuid}/builds${query}`);
77
77
  if (resp.status === 401 || resp.status === 403) {
78
78
  throw new Error('Error fetching builds. Permission denied');
79
79
  }
@@ -90,7 +90,7 @@ export class SiteClient {
90
90
  }
91
91
  async listBackups(options = {}) {
92
92
  const query = buildQuery(options);
93
- const resp = await this.#client.fetch(`/sites/${this.#uuid}/archives?${query}`);
93
+ const resp = await this.#client.fetch(`/sites/${this.#uuid}/archives${query}`);
94
94
  if (resp.status === 401) {
95
95
  throw new Error('Error fetching backups. Permission denied');
96
96
  }
@@ -140,7 +140,7 @@ export class SiteClient {
140
140
  }
141
141
  async getSyncs(options = {}) {
142
142
  const query = buildQuery(options);
143
- const resp = await this.#client.fetch(`/sites/${this.#uuid}/syncs?${query}`);
143
+ const resp = await this.#client.fetch(`/sites/${this.#uuid}/syncs${query}`);
144
144
  if (resp.status === 401) {
145
145
  throw new Error('Error fetching syncs. Permission denied');
146
146
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cloudcannon/sdk",
3
3
  "type": "module",
4
- "version": "0.0.4",
4
+ "version": "0.0.6",
5
5
  "description": "REST API client for the CloudCannon CMS.",
6
6
  "keywords": [
7
7
  "cloudcannon",