@xiboplayer/xmds 0.6.3 → 0.6.5

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
@@ -1,15 +1,42 @@
1
1
  # @xiboplayer/xmds
2
2
 
3
- **XMDS SOAP + REST client for Xibo CMS communication.**
3
+ **XMDS/REST dual-transport CMS client for Xibo digital signage -- auto-detects REST or SOAP based on CMS capabilities.**
4
4
 
5
5
  ## Overview
6
6
 
7
- Dual-transport client supporting both Xibo communication protocols:
7
+ Unified abstraction over Xibo's two communication protocols:
8
8
 
9
- - **XMDS SOAP** (v3-v7) standard Xibo player protocol with XML encoding
10
- - **REST API** lighter JSON transport (~30% smaller payloads) with ETag caching
9
+ - **REST API v2** -- JSON-based, JWT auth, ETag caching, ~30% smaller payloads
10
+ - **XMDS SOAP** (v3-v7) -- XML-based, universal compatibility with all Xibo CMS versions
11
11
 
12
- Both transports expose the same API. The REST client is preferred when available.
12
+ Both expose an identical public API. At startup, `ProtocolDetector` probes the CMS to select the optimal transport -- fallback to SOAP if REST is unavailable.
13
+
14
+ ### Capabilities
15
+
16
+ - **Dual-transport abstraction** -- RestClient and XmdsClient implement the same interface
17
+ - **Auto-detection** -- quick health probe (3s timeout) to select REST or SOAP
18
+ - **HTTP caching** -- REST client uses ETags to avoid redundant GETs
19
+ - **Retry & backoff** -- exponential backoff with jitter (default: 2 retries, 2s base delay)
20
+ - **JWT auth** -- REST client auto-refreshes tokens 60s before expiry
21
+ - **CRC checksums** -- `checkRf` and `checkSchedule` allow skipping unchanged data
22
+ - **Delegated reporting** -- stats/logs can be submitted on behalf of other displays (follower -> lead delegation)
23
+
24
+ ## Architecture
25
+
26
+ ```
27
+ Player Core Transport Selection CMS
28
+ ----------- -------------------- -----
29
+
30
+ registerDisplay() Is REST available?
31
+ requiredFiles() Same API (GET /api/v2/player/health)
32
+ schedule() Yes --> RestClient (JWT, ETag) --> /api/v2/player/*
33
+ getResource() No --> XmdsClient (SOAP XML) --> /xmds.php
34
+ notify()
35
+ submitStats()
36
+ submitLog()
37
+ ```
38
+
39
+ Both clients share the same return types. The scheduler, renderer, and sync modules consume only the CmsClient interface -- they're transport-agnostic.
13
40
 
14
41
  ## Installation
15
42
 
@@ -19,37 +46,164 @@ npm install @xiboplayer/xmds
19
46
 
20
47
  ## Usage
21
48
 
49
+ ### Auto-detect and instantiate
50
+
51
+ ```javascript
52
+ import { ProtocolDetector, RestClient, XmdsClient } from '@xiboplayer/xmds';
53
+
54
+ const detector = new ProtocolDetector(cmsUrl, RestClient, XmdsClient);
55
+ const { client, protocol } = await detector.detect({
56
+ cmsUrl: 'https://cms.example.com',
57
+ cmsKey: 'your-server-key',
58
+ hardwareKey: 'display-123',
59
+ displayName: 'Main Screen',
60
+ });
61
+
62
+ console.log(`Using ${protocol} transport`);
63
+
64
+ const display = await client.registerDisplay();
65
+ const files = await client.requiredFiles();
66
+ const schedule = await client.schedule();
67
+ ```
68
+
69
+ ### Force a specific transport
70
+
71
+ ```javascript
72
+ const { client } = await detector.detect(config, 'xmds'); // Force SOAP
73
+ const { client } = await detector.detect(config, 'rest'); // Force REST
74
+ ```
75
+
76
+ ### Direct RestClient usage
77
+
22
78
  ```javascript
23
79
  import { RestClient } from '@xiboplayer/xmds';
24
80
 
25
81
  const client = new RestClient({
26
- cmsUrl: 'https://your-cms.example.com',
27
- serverKey: 'your-key',
28
- hardwareKey: 'display-id',
82
+ cmsUrl: 'https://cms.example.com',
83
+ cmsKey: 'server-key',
84
+ hardwareKey: 'display-key',
85
+ displayName: 'Display 1',
29
86
  });
30
87
 
31
- const result = await client.registerDisplay();
32
- const files = await client.requiredFiles();
33
- const schedule = await client.schedule();
88
+ const { code, message, settings, syncConfig } = await client.registerDisplay();
89
+ const { files, purge } = await client.requiredFiles();
90
+ ```
91
+
92
+ ### Direct XmdsClient usage
93
+
94
+ ```javascript
95
+ import { XmdsClient } from '@xiboplayer/xmds';
96
+
97
+ const client = new XmdsClient({
98
+ cmsUrl: 'https://cms.example.com',
99
+ cmsKey: 'server-key',
100
+ hardwareKey: 'display-key',
101
+ displayName: 'Display 1',
102
+ xmrChannel: 'ch-123',
103
+ xmrPubKey: '-----BEGIN PUBLIC KEY-----\n...',
104
+ });
105
+
106
+ const display = await client.registerDisplay();
107
+ ```
108
+
109
+ ### Parse schedule without network call
110
+
111
+ ```javascript
112
+ import { parseScheduleResponse } from '@xiboplayer/xmds';
113
+
114
+ const parsed = parseScheduleResponse(scheduleXml);
115
+ // { default, layouts, campaigns, overlays, actions, commands, dataConnectors }
34
116
  ```
35
117
 
36
118
  ## Methods
37
119
 
38
- | Method | Description |
39
- |--------|-------------|
40
- | `registerDisplay()` | Register/authorize the display with the CMS |
41
- | `requiredFiles()` | Get list of required media files and layouts |
42
- | `schedule()` | Get the current schedule XML |
43
- | `getResource(regionId, mediaId)` | Get rendered widget HTML |
44
- | `notifyStatus(status)` | Report display status to CMS |
45
- | `mediaInventory(inventory)` | Report cached media inventory |
46
- | `submitStats(stats, hardwareKeyOverride?)` | Submit proof of play statistics (optional `hardwareKeyOverride` for delegated submissions on behalf of another display) |
47
- | `submitScreenShot(base64)` | Upload a screenshot to the CMS |
48
- | `submitLog(logs, hardwareKeyOverride?)` | Submit display logs (optional `hardwareKeyOverride` for delegated submissions on behalf of another display) |
120
+ All methods available on both `RestClient` and `XmdsClient` with identical signatures:
121
+
122
+ | Method | Parameters | Returns | Description |
123
+ |--------|------------|---------|-------------|
124
+ | `registerDisplay()` | -- | `RegisterDisplayResult` | Authenticate, get settings, tags, commands, sync config |
125
+ | `requiredFiles()` | -- | `RequiredFilesResult` | Get media, layouts, widgets, and files to purge |
126
+ | `schedule()` | -- | `ScheduleObject` | Get complete schedule |
127
+ | `getResource(layoutId, regionId, mediaId)` | number x 3 | `string` | Get rendered widget HTML |
128
+ | `notifyStatus(status)` | Object | JSON/XML | Report display status |
129
+ | `mediaInventory(inventoryXml)` | string/Array | JSON/XML | Report cached media |
130
+ | `submitStats(statsXml, hardwareKeyOverride?)` | string, string? | boolean | Submit proof-of-play (optional override for delegated reporting) |
131
+ | `submitLog(logXml, hardwareKeyOverride?)` | string, string? | boolean | Submit logs (optional override for delegated reporting) |
132
+ | `submitScreenShot(base64Image)` | string | boolean | Upload screenshot |
133
+ | `reportFaults(faultJson)` | string/Object | boolean | Report hardware/software faults |
134
+ | `blackList(mediaId, type, reason)` | string, string, string | boolean | Blacklist broken media |
135
+ | `getWeather()` | -- | JSON/XML | Get weather data for schedule criteria |
136
+
137
+ ### Key Response Types
138
+
139
+ **RegisterDisplayResult:**
140
+ ```typescript
141
+ {
142
+ code: 'READY' | 'WRONG_SCHEDULE_KEY' | 'DISPLAY_NOT_LICENSED' | ...,
143
+ message: string,
144
+ settings: { [key: string]: any },
145
+ tags: string[],
146
+ commands: Array<{ commandCode, commandString }>,
147
+ checkRf: string, // CRC32 of RequiredFiles (skip if unchanged)
148
+ checkSchedule: string, // CRC32 of Schedule (skip if unchanged)
149
+ syncConfig: { // Multi-display sync (null if not enabled)
150
+ syncGroup: string,
151
+ syncPublisherPort: number,
152
+ syncSwitchDelay: number,
153
+ isLead: boolean
154
+ } | null
155
+ }
156
+ ```
157
+
158
+ ## Transport Comparison
159
+
160
+ | Aspect | REST (v2) | XMDS (SOAP) |
161
+ |--------|-----------|-------------|
162
+ | **Protocol** | JSON over HTTP | XML-RPC over HTTP |
163
+ | **Auth** | JWT (Bearer token) | Per-request params (serverKey) |
164
+ | **Payload size** | ~30% smaller | Baseline |
165
+ | **Caching** | ETags + response cache | No caching |
166
+ | **Availability** | Custom CMS images (Xibo 3.0+) | All Xibo versions (v3-v7+) |
167
+ | **Fallback** | SOAP (automatic) | None |
168
+
169
+ ## Error Handling
170
+
171
+ **Retry:** Both clients use `fetchWithRetry()` with exponential backoff (2 retries, 2s base delay).
172
+
173
+ **Token expiry (REST):** 401 response triggers automatic re-authentication and request retry.
174
+
175
+ **SOAP faults:** XmdsClient parses `<soap:Fault>` and throws with fault message.
176
+
177
+ **Custom retry strategy:**
178
+ ```javascript
179
+ const client = new RestClient({
180
+ ...config,
181
+ retryOptions: { maxRetries: 5, baseDelayMs: 5000 }
182
+ });
183
+ ```
184
+
185
+ ## Constructor Options
186
+
187
+ ```typescript
188
+ {
189
+ cmsUrl: string, // Base URL of Xibo CMS
190
+ cmsKey: string, // Server authentication key
191
+ hardwareKey: string, // Unique display identifier
192
+ displayName?: string, // Human-readable display name
193
+ clientVersion?: string, // Default: '0.1.0'
194
+ clientType?: string, // Default: 'linux'
195
+ xmrChannel?: string, // XMR channel ID
196
+ xmrPubKey?: string, // XMR public key (PEM)
197
+ retryOptions?: {
198
+ maxRetries?: number, // Default: 2
199
+ baseDelayMs?: number // Default: 2000
200
+ }
201
+ }
202
+ ```
49
203
 
50
204
  ## Dependencies
51
205
 
52
- - `@xiboplayer/utils` logger, events, fetchWithRetry
206
+ - `@xiboplayer/utils` -- logger, fetchWithRetry
53
207
 
54
208
  ---
55
209
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiboplayer/xmds",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "XMDS SOAP client for Xibo CMS communication",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -12,7 +12,7 @@
12
12
  "./schedule-parser": "./src/schedule-parser.js"
13
13
  },
14
14
  "dependencies": {
15
- "@xiboplayer/utils": "0.6.3"
15
+ "@xiboplayer/utils": "0.6.5"
16
16
  },
17
17
  "devDependencies": {
18
18
  "vitest": "^2.0.0"
@@ -0,0 +1,89 @@
1
+ /**
2
+ * CmsClient interface definition — the unified contract for CMS communication.
3
+ *
4
+ * Both RestClient and XmdsClient implement this interface identically.
5
+ * PlayerCore calls these methods without knowing which transport is underneath.
6
+ * ProtocolDetector selects the implementation at startup.
7
+ *
8
+ * This file provides:
9
+ * 1. JSDoc type definitions for the interface
10
+ * 2. CMS_CLIENT_METHODS — canonical list of required methods
11
+ * 3. assertCmsClient() — runtime conformance check
12
+ *
13
+ * @example
14
+ * import { ProtocolDetector, RestClient, XmdsClient } from '@xiboplayer/xmds';
15
+ *
16
+ * const detector = new ProtocolDetector(cmsUrl, RestClient, XmdsClient);
17
+ * const { client } = await detector.detect(config);
18
+ * // client implements CmsClient — PlayerCore doesn't care which one
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} RegisterDisplayResult
23
+ * @property {string} code - 'READY' or error code
24
+ * @property {string} message - Human-readable status message
25
+ * @property {Object|null} settings - Display settings (null if not READY)
26
+ * @property {string[]} tags - Display tags
27
+ * @property {Array<{commandCode: string, commandString: string}>} commands - Scheduled commands
28
+ * @property {Object} displayAttrs - Server-provided attributes (date, timezone, status)
29
+ * @property {string} checkRf - CRC checksum for RequiredFiles (skip if unchanged)
30
+ * @property {string} checkSchedule - CRC checksum for Schedule (skip if unchanged)
31
+ * @property {Object|null} syncConfig - Multi-display sync configuration
32
+ */
33
+
34
+ /**
35
+ * @typedef {Object} RequiredFilesResult
36
+ * @property {Array<{type: string, id: string, size: number, md5: string, download: string, path: string, saveAs: string|null}>} files
37
+ * @property {Array<{id: string, storedAs: string}>} purge - Files to delete
38
+ */
39
+
40
+ /**
41
+ * @typedef {Object} CmsClient
42
+ * @property {() => Promise<RegisterDisplayResult>} registerDisplay
43
+ * @property {() => Promise<RequiredFilesResult>} requiredFiles
44
+ * @property {() => Promise<Object>} schedule
45
+ * @property {(layoutId: string, regionId: string, mediaId: string) => Promise<string>} getResource
46
+ * @property {(status: Object) => Promise<any>} notifyStatus
47
+ * @property {(inventoryXml: string|Array) => Promise<any>} mediaInventory
48
+ * @property {(mediaId: string, type: string, reason: string) => Promise<boolean>} blackList
49
+ * @property {(logXml: string|Array, hardwareKeyOverride?: string) => Promise<boolean>} submitLog
50
+ * @property {(base64Image: string) => Promise<boolean>} submitScreenShot
51
+ * @property {(statsXml: string|Array, hardwareKeyOverride?: string) => Promise<boolean>} submitStats
52
+ * @property {(faultJson: string|Object) => Promise<boolean>} reportFaults
53
+ * @property {() => Promise<Object>} getWeather
54
+ */
55
+
56
+ /**
57
+ * Canonical list of methods that every CmsClient implementation must provide.
58
+ * Used for runtime conformance checks and test assertions.
59
+ */
60
+ export const CMS_CLIENT_METHODS = [
61
+ 'registerDisplay',
62
+ 'requiredFiles',
63
+ 'schedule',
64
+ 'getResource',
65
+ 'notifyStatus',
66
+ 'mediaInventory',
67
+ 'blackList',
68
+ 'submitLog',
69
+ 'submitScreenShot',
70
+ 'submitStats',
71
+ 'reportFaults',
72
+ 'getWeather',
73
+ ];
74
+
75
+ /**
76
+ * Verify that an object implements the CmsClient interface.
77
+ * Throws if any required method is missing or not a function.
78
+ *
79
+ * @param {any} client - Object to check
80
+ * @param {string} [label] - Label for error messages (e.g. 'RestClient')
81
+ * @throws {Error} If the client doesn't conform
82
+ */
83
+ export function assertCmsClient(client, label = 'client') {
84
+ for (const method of CMS_CLIENT_METHODS) {
85
+ if (typeof client[method] !== 'function') {
86
+ throw new Error(`${label} missing CmsClient method: ${method}()`);
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,51 @@
1
+ // @vitest-environment node
2
+ import { describe, it, expect } from 'vitest';
3
+ import { CMS_CLIENT_METHODS, assertCmsClient } from './cms-client.js';
4
+ import { RestClient } from './rest-client.js';
5
+ import { XmdsClient } from './xmds-client.js';
6
+
7
+ const mockConfig = {
8
+ cmsUrl: 'https://example.com',
9
+ cmsKey: 'test',
10
+ hardwareKey: 'hw-001',
11
+ displayName: 'Test Display',
12
+ xmrChannel: '',
13
+ };
14
+
15
+ describe('CmsClient interface conformance', () => {
16
+ const restClient = new RestClient(mockConfig);
17
+ const xmdsClient = new XmdsClient(mockConfig);
18
+
19
+ it('CMS_CLIENT_METHODS lists 12 methods', () => {
20
+ expect(CMS_CLIENT_METHODS).toHaveLength(12);
21
+ });
22
+
23
+ for (const method of CMS_CLIENT_METHODS) {
24
+ it(`RestClient implements ${method}()`, () => {
25
+ expect(typeof restClient[method]).toBe('function');
26
+ });
27
+
28
+ it(`XmdsClient implements ${method}()`, () => {
29
+ expect(typeof xmdsClient[method]).toBe('function');
30
+ });
31
+ }
32
+
33
+ it('assertCmsClient passes for RestClient', () => {
34
+ expect(() => assertCmsClient(restClient, 'RestClient')).not.toThrow();
35
+ });
36
+
37
+ it('assertCmsClient passes for XmdsClient', () => {
38
+ expect(() => assertCmsClient(xmdsClient, 'XmdsClient')).not.toThrow();
39
+ });
40
+
41
+ it('assertCmsClient throws for incomplete client', () => {
42
+ const partial = { registerDisplay: () => {} };
43
+ expect(() => assertCmsClient(partial, 'partial')).toThrow('partial missing CmsClient method');
44
+ });
45
+
46
+ it('assertCmsClient throws for non-function property', () => {
47
+ const bad = Object.fromEntries(CMS_CLIENT_METHODS.map(m => [m, () => {}]));
48
+ bad.schedule = 'not a function';
49
+ expect(() => assertCmsClient(bad, 'bad')).toThrow('bad missing CmsClient method: schedule()');
50
+ });
51
+ });
package/src/index.js CHANGED
@@ -4,4 +4,5 @@ export const VERSION = pkg.version;
4
4
  export { RestClient } from './rest-client.js';
5
5
  export { XmdsClient } from './xmds-client.js';
6
6
  export { ProtocolDetector } from './protocol-detector.js';
7
+ export { CMS_CLIENT_METHODS, assertCmsClient } from './cms-client.js';
7
8
  export { parseScheduleResponse } from './schedule-parser.js';
@@ -2,11 +2,11 @@
2
2
  * CMS Protocol Auto-Detector
3
3
  *
4
4
  * Probes the CMS to determine which communication protocol to use:
5
- * - REST/PlayerApiV2 — optimized JSON protocol (custom CMS image)
5
+ * - REST/PlayerRestApi — optimized JSON protocol (custom CMS image)
6
6
  * - SOAP/XMDS — universal XML protocol (any vanilla Xibo CMS)
7
7
  *
8
8
  * Detection logic:
9
- * 1. GET {cmsUrl}/api/v2/player/health with a 3-second timeout
9
+ * 1. GET {cmsUrl}${PLAYER_API}/health with a 3-second timeout
10
10
  * 2. If 200 + valid JSON → REST
11
11
  * 3. If 404/error/timeout → SOAP (fallback)
12
12
  *
@@ -25,6 +25,7 @@
25
25
  */
26
26
 
27
27
  import { createLogger } from '@xiboplayer/utils';
28
+ import { assertCmsClient } from './cms-client.js';
28
29
 
29
30
  const log = createLogger('Protocol');
30
31
 
@@ -54,7 +55,7 @@ export class ProtocolDetector {
54
55
 
55
56
  /**
56
57
  * Probe the CMS health endpoint to determine protocol availability.
57
- * @returns {Promise<boolean>} true if REST/PlayerApiV2 is available
58
+ * @returns {Promise<boolean>} true if REST/PlayerRestApi is available
58
59
  */
59
60
  async probe() {
60
61
  const available = await this.RestClient.isAvailable(this.cmsUrl, {
@@ -78,13 +79,17 @@ export class ProtocolDetector {
78
79
  if (forceProtocol === 'rest') {
79
80
  this.protocol = 'rest';
80
81
  log.info('Using REST transport (forced)');
81
- return { client: new this.RestClient(config), protocol: 'rest' };
82
+ const client = new this.RestClient(config);
83
+ assertCmsClient(client, 'RestClient');
84
+ return { client, protocol: 'rest' };
82
85
  }
83
86
 
84
87
  if (forceProtocol === 'xmds') {
85
88
  this.protocol = 'xmds';
86
89
  log.info('Using XMDS/SOAP transport (forced)');
87
- return { client: new this.XmdsClient(config), protocol: 'xmds' };
90
+ const client = new this.XmdsClient(config);
91
+ assertCmsClient(client, 'XmdsClient');
92
+ return { client, protocol: 'xmds' };
88
93
  }
89
94
 
90
95
  // Auto-detect
@@ -98,13 +103,17 @@ export class ProtocolDetector {
98
103
 
99
104
  if (isRest) {
100
105
  this.protocol = 'rest';
101
- log.info('REST transport detected — using PlayerApiV2');
102
- return { client: new this.RestClient(config), protocol: 'rest' };
106
+ log.info('REST transport detected — using PlayerRestApi');
107
+ const client = new this.RestClient(config);
108
+ assertCmsClient(client, 'RestClient');
109
+ return { client, protocol: 'rest' };
103
110
  }
104
111
 
105
112
  this.protocol = 'xmds';
106
113
  log.info('REST unavailable — using XMDS/SOAP transport');
107
- return { client: new this.XmdsClient(config), protocol: 'xmds' };
114
+ const client = new this.XmdsClient(config);
115
+ assertCmsClient(client, 'XmdsClient');
116
+ return { client, protocol: 'xmds' };
108
117
  }
109
118
 
110
119
  /**
@@ -132,6 +141,7 @@ export class ProtocolDetector {
132
141
  log.info(`Protocol changed: ${previousProtocol} → ${newProtocol}`);
133
142
  this.protocol = newProtocol;
134
143
  const client = isRest ? new this.RestClient(config) : new this.XmdsClient(config);
144
+ assertCmsClient(client, isRest ? 'RestClient' : 'XmdsClient');
135
145
  return { client, protocol: newProtocol, changed: true };
136
146
  }
137
147
 
@@ -10,6 +10,14 @@
10
10
 
11
11
  import { describe, it, expect, beforeEach, vi } from 'vitest';
12
12
  import { ProtocolDetector } from './protocol-detector.js';
13
+ import { CMS_CLIENT_METHODS } from './cms-client.js';
14
+
15
+ // Stub all CmsClient methods on a mock constructor's prototype
16
+ function addCmsClientStubs(MockClass) {
17
+ for (const method of CMS_CLIENT_METHODS) {
18
+ MockClass.prototype[method] = vi.fn();
19
+ }
20
+ }
13
21
 
14
22
  describe('ProtocolDetector', () => {
15
23
  let MockRestClient;
@@ -28,11 +36,13 @@ describe('ProtocolDetector', () => {
28
36
  this.type = 'rest';
29
37
  });
30
38
  MockRestClient.isAvailable = vi.fn();
39
+ addCmsClientStubs(MockRestClient);
31
40
 
32
41
  MockXmdsClient = vi.fn(function (cfg) {
33
42
  this.config = cfg;
34
43
  this.type = 'xmds';
35
44
  });
45
+ addCmsClientStubs(MockXmdsClient);
36
46
  });
37
47
 
38
48
  // ── detect() ─────────────────────────────────────────────────────
@@ -513,7 +513,7 @@ export class RestClient {
513
513
 
514
514
  /**
515
515
  * Probe whether the CMS supports API v2.
516
- * GET /api/v2/player/health → { version: 2, status: "ok" }
516
+ * GET ${PLAYER_API}/health → { version: 2, status: "ok" }
517
517
  *
518
518
  * @param {string} cmsUrl - CMS base URL
519
519
  * @param {Object} [retryOptions] - Retry options for fetch
@@ -5,7 +5,7 @@
5
5
  * with the Player API module deployed.
6
6
  *
7
7
  * Prerequisites:
8
- * - CMS at CMS_URL must have /api/v2/player/* endpoints deployed
8
+ * - CMS at CMS_URL must have ${PLAYER_API}/* endpoints deployed
9
9
  * - A display with the given HARDWARE_KEY must exist and be authorized
10
10
  * - The SERVER_KEY must match the CMS setting
11
11
  *