@raketa-cloud/admin 1.9.5 → 1.9.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.
@@ -749,40 +749,6 @@ select {
749
749
  --tw-contain-style: ;
750
750
  }
751
751
 
752
- .container {
753
- width: 100%;
754
- }
755
-
756
- @media (min-width: 640px) {
757
- .container {
758
- max-width: 640px;
759
- }
760
- }
761
-
762
- @media (min-width: 768px) {
763
- .container {
764
- max-width: 768px;
765
- }
766
- }
767
-
768
- @media (min-width: 1024px) {
769
- .container {
770
- max-width: 1024px;
771
- }
772
- }
773
-
774
- @media (min-width: 1280px) {
775
- .container {
776
- max-width: 1280px;
777
- }
778
- }
779
-
780
- @media (min-width: 1536px) {
781
- .container {
782
- max-width: 1536px;
783
- }
784
- }
785
-
786
752
  /* Utils */
787
753
 
788
754
  .chip {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raketa-cloud/admin",
3
- "version": "1.9.5",
3
+ "version": "1.9.6",
4
4
  "main": "src/index.js",
5
5
  "files": [
6
6
  "src/",
@@ -37,7 +37,7 @@
37
37
  "chart.js": "4.5.1",
38
38
  "ejs": "6.0.1",
39
39
  "graphql-request": "7.4.0",
40
- "hono": "4.12.34",
40
+ "hono": "4.13.7",
41
41
  "lru-cache": "11.5.2"
42
42
  },
43
43
  "overrides": {
@@ -156,7 +156,7 @@ export default class Resources extends RaketaClient {
156
156
  return { resource, errors };
157
157
  }
158
158
 
159
- appendWorkflow(input, workflow) {
159
+ appendWorkflow(input, workflow, previewParams = {}) {
160
160
  if (!workflow?.type) {
161
161
  return;
162
162
  }
@@ -165,9 +165,9 @@ export default class Resources extends RaketaClient {
165
165
  type: workflow.type,
166
166
  previewUrl: buildMagicPreviewAddress(
167
167
  {
168
- slug: input.resource.slug,
169
- locale: input.locale,
170
- resourceType: input.resource.resourceType,
168
+ slug: previewParams.slug,
169
+ locale: previewParams.locale,
170
+ resourceType: previewParams.resourceType,
171
171
  },
172
172
  this.getConfig(),
173
173
  ),
@@ -200,23 +200,11 @@ export default class Resources extends RaketaClient {
200
200
  translations,
201
201
  };
202
202
 
203
- if (workflow?.type) {
204
- input.workflow = {
205
- type: workflow.type,
206
- previewUrl: buildMagicPreviewAddress(
207
- {
208
- slug: paramsCopy.slug,
209
- locale: sourceLocale || locales[0],
210
- resourceType,
211
- },
212
- this.getConfig(),
213
- ),
214
- };
215
-
216
- if (workflow.due_date) {
217
- input.workflow.dueDate = toUTCISO8601String(workflow.due_date);
218
- }
219
- }
203
+ this.appendWorkflow(input, workflow, {
204
+ slug: paramsCopy.slug,
205
+ locale: sourceLocale,
206
+ resourceType,
207
+ });
220
208
 
221
209
  const {
222
210
  multiTranslateResource: { resources, errors, localeErrors },
@@ -44,16 +44,7 @@ function resourceFragment() {
44
44
  tags ${tagFragment()}
45
45
  categories ${categoryFragment()}
46
46
  changes ${summaryChangeFragment()}
47
- activeContentTask {
48
- id
49
- workflow
50
- operator
51
- operatorMeta
52
- completedAt
53
- failedAt
54
- insertedAt
55
- updatedAt
56
- }
47
+ activeContentTask ${contentTaskFragment()}
57
48
  }
58
49
  `;
59
50
  }
@@ -8,9 +8,10 @@ import {
8
8
  const ALGORITHM = "aes-256-gcm";
9
9
  const IV_LENGTH = 12;
10
10
  const TAG_LENGTH = 16;
11
+ const CHUNK_SIZE = 40;
11
12
 
12
13
  function getSecretKey(config = {}) {
13
- const secret = config.secrets.crypto_secret;
14
+ const secret = config.secrets?.crypto_secret;
14
15
 
15
16
  if (!secret) {
16
17
  throw new Error(
@@ -21,6 +22,40 @@ function getSecretKey(config = {}) {
21
22
  return createHash("sha256").update(secret).digest();
22
23
  }
23
24
 
25
+ export function chunkToken(token, chunkSize = CHUNK_SIZE) {
26
+ if (!token || typeof token !== "string") {
27
+ return token;
28
+ }
29
+
30
+ const chunks = [];
31
+ for (let i = 0; i < token.length; i += chunkSize) {
32
+ chunks.push(token.slice(i, i + chunkSize));
33
+ }
34
+
35
+ return chunks.join("-");
36
+ }
37
+
38
+ export function unchunkToken(token, chunkSize = CHUNK_SIZE) {
39
+ if (!token || typeof token !== "string") {
40
+ return token;
41
+ }
42
+
43
+ let result = "";
44
+ let i = 0;
45
+
46
+ while (i < token.length) {
47
+ const chunk = token.slice(i, i + chunkSize);
48
+ result += chunk;
49
+ i += chunkSize;
50
+
51
+ if (i < token.length && token[i] === "-") {
52
+ i += 1;
53
+ }
54
+ }
55
+
56
+ return result;
57
+ }
58
+
24
59
  export function generateMagicPreviewToken(
25
60
  { slug, resourceType, locale } = {},
26
61
  config = {},
@@ -40,17 +75,14 @@ export function generateMagicPreviewToken(
40
75
  ]);
41
76
  const tag = cipher.getAuthTag();
42
77
 
43
- return Buffer.concat([iv, tag, encrypted]).toString("base64url");
44
- }
78
+ const rawToken = Buffer.concat([iv, tag, encrypted]).toString("base64url");
45
79
 
46
- export function decipherMagicPreviewToken(token, config) {
47
- if (!token || typeof token !== "string") {
48
- return null;
49
- }
80
+ return chunkToken(rawToken);
81
+ }
50
82
 
83
+ function decipherPayload(rawToken, key) {
51
84
  try {
52
- const key = getSecretKey(config);
53
- const combined = Buffer.from(token, "base64url");
85
+ const combined = Buffer.from(rawToken, "base64url");
54
86
 
55
87
  if (combined.length < IV_LENGTH + TAG_LENGTH) {
56
88
  return null;
@@ -80,6 +112,31 @@ export function decipherMagicPreviewToken(token, config) {
80
112
  }
81
113
  }
82
114
 
115
+ export function decipherMagicPreviewToken(token, config) {
116
+ if (!token || typeof token !== "string") {
117
+ return null;
118
+ }
119
+
120
+ try {
121
+ const key = getSecretKey(config);
122
+
123
+ const unchunked = unchunkToken(token);
124
+ const payload = decipherPayload(unchunked, key);
125
+
126
+ if (payload) {
127
+ return payload;
128
+ }
129
+
130
+ if (unchunked !== token) {
131
+ return decipherPayload(token, key);
132
+ }
133
+
134
+ return null;
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+
83
140
  export function buildMagicPreviewAddress(
84
141
  { slug, resourceType, locale },
85
142
  globalConfig,
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  buildMagicPreviewAddress,
3
+ chunkToken,
3
4
  decipherMagicPreviewToken,
4
5
  generateMagicPreviewToken,
6
+ unchunkToken,
5
7
  } from "../magicPreview";
6
8
 
7
9
  describe("magicPreview helper", () => {
@@ -104,6 +106,13 @@ describe("magicPreview helper", () => {
104
106
  expect(typeof token).toBe("string");
105
107
  expect(token).not.toContain("my-first-ebook");
106
108
 
109
+ // Verify token contains dashes breaking it into chunks of at most 40 characters
110
+ const chunks = token.split("-");
111
+ expect(chunks.length).toBeGreaterThan(1);
112
+ for (const chunk of chunks) {
113
+ expect(chunk.length).toBeLessThanOrEqual(40);
114
+ }
115
+
107
116
  const payload = decipherMagicPreviewToken(token, config);
108
117
  expect(payload).not.toBeNull();
109
118
  expect(payload.slug).toBe("my-first-ebook");
@@ -111,9 +120,40 @@ describe("magicPreview helper", () => {
111
120
  expect(payload.locale).toBe("en");
112
121
  });
113
122
 
123
+ it("deciphers legacy unchunked tokens without dashes", () => {
124
+ // Manually produce a legacy unchunked token
125
+ const {
126
+ createCipheriv,
127
+ createHash,
128
+ randomBytes,
129
+ } = require("node:crypto");
130
+ const key = createHash("sha256")
131
+ .update(config.secrets.crypto_secret)
132
+ .digest();
133
+ const iv = randomBytes(12);
134
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
135
+ const payload = {
136
+ slug: "legacy-post",
137
+ resourceType: "post",
138
+ locale: "de",
139
+ };
140
+ const encrypted = Buffer.concat([
141
+ cipher.update(JSON.stringify(payload), "utf8"),
142
+ cipher.final(),
143
+ ]);
144
+ const tag = cipher.getAuthTag();
145
+ const legacyToken = Buffer.concat([iv, tag, encrypted]).toString(
146
+ "base64url",
147
+ );
148
+
149
+ const deciphered = decipherMagicPreviewToken(legacyToken, config);
150
+ expect(deciphered).toEqual(payload);
151
+ });
152
+
114
153
  it("returns null on invalid / tampered token", () => {
115
154
  expect(decipherMagicPreviewToken("", config)).toBeNull();
116
155
  expect(decipherMagicPreviewToken(null, config)).toBeNull();
156
+ expect(decipherMagicPreviewToken(123, config)).toBeNull();
117
157
  expect(decipherMagicPreviewToken("invalid-token", config)).toBeNull();
118
158
 
119
159
  const token = generateMagicPreviewToken(
@@ -124,4 +164,43 @@ describe("magicPreview helper", () => {
124
164
  expect(decipherMagicPreviewToken(tampered, config)).toBeNull();
125
165
  });
126
166
  });
167
+
168
+ describe("chunkToken and unchunkToken helpers", () => {
169
+ it("handles non-string or falsy input gracefully", () => {
170
+ expect(chunkToken(null)).toBeNull();
171
+ expect(chunkToken(undefined)).toBeUndefined();
172
+ expect(chunkToken("")).toBe("");
173
+ expect(unchunkToken(null)).toBeNull();
174
+ expect(unchunkToken(undefined)).toBeUndefined();
175
+ expect(unchunkToken("")).toBe("");
176
+ });
177
+
178
+ it("chunks string into 40-char segments joined by dashes", () => {
179
+ const input = "a".repeat(40) + "b".repeat(40) + "c".repeat(15);
180
+ const chunked = chunkToken(input, 40);
181
+
182
+ expect(chunked).toBe(
183
+ "a".repeat(40) + "-" + "b".repeat(40) + "-" + "c".repeat(15),
184
+ );
185
+ expect(unchunkToken(chunked, 40)).toBe(input);
186
+ });
187
+
188
+ it("leaves string <= chunkSize unchanged", () => {
189
+ const input = "a".repeat(35);
190
+
191
+ expect(chunkToken(input, 40)).toBe(input);
192
+ expect(unchunkToken(input, 40)).toBe(input);
193
+ });
194
+
195
+ it("handles strings with existing dashes inside chunks", () => {
196
+ const input = "abc-def-123".repeat(10);
197
+ const chunked = chunkToken(input, 40);
198
+
199
+ expect(chunked).toBe(
200
+ "abc-def-123abc-def-123abc-def-123abc-def--123abc-def-123abc-def-123abc-def-123abc--def-123abc-def-123abc-def-123",
201
+ );
202
+
203
+ expect(unchunkToken(chunked, 40)).toBe(input);
204
+ });
205
+ });
127
206
  });
@@ -9,6 +9,7 @@
9
9
  <th>Workflow</th>
10
10
  <th>Started at</th>
11
11
  <% if (showAllDetails) { %>
12
+ <th>Due date</th>
12
13
  <th>Finished at</th>
13
14
  <% } %>
14
15
  <th>Status</th>
@@ -84,6 +85,23 @@
84
85
  </td>
85
86
 
86
87
  <% if (showAllDetails) { %>
88
+ <td>
89
+ <div class="body-3 text-raketa-foreground/50">
90
+ <% const dueDate = task.lokaliseWorkflow?.settings?.due_date %>
91
+
92
+ <% if (dueDate) { %>
93
+ <div class="flex items-center gap-1 mb-0.5">
94
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4 text-raketa-foreground">
95
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
96
+ </svg>
97
+ <time data-pretty-datetime datetime="<%= dueDate %>"><%= utils.formattedDateTime(dueDate) %></time>
98
+ </div>
99
+ <% } else { %>
100
+ <span class="text-raketa-foreground/50">—</span>
101
+ <% } %>
102
+ </div>
103
+ </td>
104
+
87
105
  <td>
88
106
  <% if (task.completedAt || task.failedAt) { %>
89
107
  <div class="body-3 text-raketa-foreground/50">