@paroicms/site-generator-plugin 0.8.0 → 0.9.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.
Files changed (25) hide show
  1. package/gen-backend/dist/commands/actions.js +49 -0
  2. package/gen-backend/dist/db/db.queries.js +3 -3
  3. package/gen-backend/dist/errors.js +20 -0
  4. package/gen-backend/dist/generator/actions.js +45 -0
  5. package/gen-backend/dist/generator/fake-content-generator.ts/augment-fields.js +51 -0
  6. package/gen-backend/dist/generator/fake-content-generator.ts/content-helpers.js +17 -0
  7. package/gen-backend/dist/generator/fake-content-generator.ts/create-database-with-fake-content.js +7 -4
  8. package/gen-backend/dist/generator/generator-session.js +33 -0
  9. package/gen-backend/dist/generator/generator-types.js +1 -0
  10. package/gen-backend/dist/generator/lib/parse-llm-response.js +10 -16
  11. package/gen-backend/dist/generator/lib/token-tracking.js +118 -0
  12. package/gen-backend/dist/generator/llm-queries/invoke-new-site-analysis.js +24 -5
  13. package/gen-backend/dist/generator/session/generator-session.js +33 -0
  14. package/gen-backend/dist/generator/session/session-command.js +17 -0
  15. package/gen-backend/dist/generator/site-generator/theme-scss.js +262 -0
  16. package/gen-backend/dist/generator/site-schema-generator/create-site-schema.js +1 -0
  17. package/gen-backend/dist/lib/generator-context.js +14 -0
  18. package/gen-backend/dist/plugin.js +2 -2
  19. package/gen-backend/prompts/0-context.md +4 -2
  20. package/gen-backend/prompts/new-site-1-analysis.md +23 -7
  21. package/gen-backend/prompts/new-site-2-fields.md +1 -0
  22. package/gen-backend/prompts/update-site-schema-2-execute.md +20 -4
  23. package/gen-front/dist/gen-front.css +1 -1
  24. package/gen-front/dist/gen-front.mjs +64 -64
  25. package/package.json +3 -3
@@ -0,0 +1,49 @@
1
+ import { invokeMessageGuard } from "../generator/llm-queries/invoke-message-guard.js";
2
+ import { invokeNewSiteAnalysis } from "../generator/llm-queries/invoke-new-site-analysis.js";
3
+ import { invokeUpdateSiteSchema } from "../generator/llm-queries/invoke-update-site-schema.js";
4
+ import { generateSite } from "../generator/site-generator/site-generator.js";
5
+ import { newSession, verifySessionToken } from "./generator-session.js";
6
+ export async function executeCommand(commandCtx, input) {
7
+ if (input.command === "newSession") {
8
+ return {
9
+ success: true,
10
+ result: await newSession(commandCtx, input),
11
+ };
12
+ }
13
+ const { sessionId } = verifySessionToken(commandCtx, input.sessionToken);
14
+ const ctx = {
15
+ ...commandCtx,
16
+ sessionId,
17
+ };
18
+ if (input.command === "createSiteSchema") {
19
+ const errorResponse = await invokeMessageGuard(ctx, input);
20
+ if (errorResponse)
21
+ return errorResponse;
22
+ return {
23
+ success: true,
24
+ result: await invokeNewSiteAnalysis(ctx, input),
25
+ };
26
+ }
27
+ if (input.command === "updateSiteSchema") {
28
+ const errorResponse = await invokeMessageGuard(ctx, input, {
29
+ skipOutOfScopeCheck: true,
30
+ });
31
+ if (errorResponse)
32
+ return errorResponse;
33
+ return {
34
+ success: true,
35
+ result: await invokeUpdateSiteSchema(ctx, input),
36
+ };
37
+ }
38
+ if (input.command === "generateSite") {
39
+ return {
40
+ success: true,
41
+ result: await generateSite(ctx, input),
42
+ };
43
+ }
44
+ ctx.logger.error(`Invalid command: ${input.command}`);
45
+ return {
46
+ success: false,
47
+ userMessage: "Invalid command",
48
+ };
49
+ }
@@ -1,8 +1,8 @@
1
- import { strVal } from "@paroi/data-formatters-lib";
1
+ import { strVal, strValOrUndef } from "@paroi/data-formatters-lib";
2
2
  export async function readSession(ctx, sessionId) {
3
3
  const { cn } = ctx;
4
4
  const row = await cn("PaGenSession")
5
- .select("id", "status", "guardCount", "promptCount", "nodeTypeCount", "contentCount", "errorMessage")
5
+ .select("id", "createdAt", "status", "guardCount", "promptCount", "nodeTypeCount", "contentCount", "errorMessage")
6
6
  .where({ id: sessionId })
7
7
  .first();
8
8
  if (!row)
@@ -15,7 +15,7 @@ export async function readSession(ctx, sessionId) {
15
15
  promptCount: Number(row.promptCount),
16
16
  nodeTypeCount: Number(row.nodeTypeCount),
17
17
  contentCount: Number(row.contentCount),
18
- errorMessage: strVal(row.errorMessage),
18
+ errorMessage: strValOrUndef(row.errorMessage),
19
19
  };
20
20
  }
21
21
  export async function insertSession(ctx, { sessionId }) {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Custom API error class for handling API-specific errors
3
+ */
4
+ export class ApiError extends Error {
5
+ statusCode;
6
+ constructor(message, statusCode = 400) {
7
+ super(message);
8
+ this.name = "ApiError";
9
+ this.statusCode = statusCode;
10
+ }
11
+ }
12
+ /**
13
+ * Create a new API error
14
+ * @param message Error message
15
+ * @param statusCode HTTP status code (default: 400)
16
+ * @returns ApiError instance
17
+ */
18
+ export function createApiError(message, statusCode = 400) {
19
+ return new ApiError(message, statusCode);
20
+ }
@@ -0,0 +1,45 @@
1
+ import { invokeMessageGuard } from "./llm-queries/invoke-message-guard.js";
2
+ import { invokeNewSiteAnalysis } from "./llm-queries/invoke-new-site-analysis.js";
3
+ import { invokeUpdateSiteSchema } from "./llm-queries/invoke-update-site-schema.js";
4
+ import { newSession, verifySessionToken } from "./session/generator-session.js";
5
+ import { generateSite } from "./site-generator/site-generator.js";
6
+ export async function executeCommand(ctx, input) {
7
+ if (input.command === "newSession") {
8
+ return {
9
+ success: true,
10
+ result: await newSession(ctx, input),
11
+ };
12
+ }
13
+ verifySessionToken(input.sessionToken);
14
+ if (input.command === "createSiteSchema") {
15
+ const errorResponse = await invokeMessageGuard(ctx, input);
16
+ if (errorResponse)
17
+ return errorResponse;
18
+ return {
19
+ success: true,
20
+ result: await invokeNewSiteAnalysis(ctx, input),
21
+ };
22
+ }
23
+ if (input.command === "updateSiteSchema") {
24
+ const errorResponse = await invokeMessageGuard(ctx, input, {
25
+ skipOutOfScopeCheck: true,
26
+ });
27
+ if (errorResponse)
28
+ return errorResponse;
29
+ return {
30
+ success: true,
31
+ result: await invokeUpdateSiteSchema(ctx, input),
32
+ };
33
+ }
34
+ if (input.command === "generateSite") {
35
+ return {
36
+ success: true,
37
+ result: await generateSite(ctx, input),
38
+ };
39
+ }
40
+ ctx.logger.error(`Invalid command: ${input.command}`);
41
+ return {
42
+ success: false,
43
+ userMessage: "Invalid command",
44
+ };
45
+ }
@@ -0,0 +1,51 @@
1
+ import { getRandomImagePath } from "../lib/images-lib.js";
2
+ export function augmentWithComputedValues(list, nodeType, language) {
3
+ if (!nodeType.fields)
4
+ return;
5
+ for (const item of list) {
6
+ for (const f of nodeType.fields) {
7
+ const languageKey = f.storedOn === "node" ? "." : language;
8
+ if (f.storedAs === "mediaHandle") {
9
+ const value = getMediaGeneratedFieldContent(f, language);
10
+ if (value) {
11
+ item[f.name] = value;
12
+ }
13
+ }
14
+ else {
15
+ const value = getDefaultValueForField(f.name);
16
+ if (value !== undefined) {
17
+ item[f.name] = { [languageKey]: value };
18
+ }
19
+ }
20
+ }
21
+ }
22
+ }
23
+ function getMediaGeneratedFieldContent(f, language) {
24
+ const languageKey = f.storedOn === "node" ? "." : language;
25
+ if (f.dataType === "media") {
26
+ return { [languageKey]: { file: getRandomImagePath() } };
27
+ }
28
+ if (f.dataType === "gallery") {
29
+ return {
30
+ [languageKey]: {
31
+ files: [
32
+ getRandomImagePath(),
33
+ getRandomImagePath(),
34
+ getRandomImagePath(),
35
+ getRandomImagePath(),
36
+ getRandomImagePath(),
37
+ getRandomImagePath(),
38
+ ],
39
+ },
40
+ };
41
+ }
42
+ }
43
+ function getDefaultValueForField(fieldName) {
44
+ if (fieldName === "phone" || fieldName === "phone2")
45
+ return "0123456789";
46
+ if (fieldName === "phones")
47
+ return JSON.stringify(["0123456789", "9876543210"]);
48
+ if (fieldName === "updateDateTime")
49
+ return new Date().toISOString();
50
+ return undefined;
51
+ }
@@ -0,0 +1,17 @@
1
+ export function dedupMessages(messages) {
2
+ const counters = new Map();
3
+ const result = [];
4
+ for (const m of messages) {
5
+ const counter = counters.get(m);
6
+ if (counter) {
7
+ counters.set(m, counter + 1);
8
+ continue;
9
+ }
10
+ counters.set(m, 1);
11
+ result.push(m);
12
+ }
13
+ return result.map((m) => {
14
+ const counter = counters.get(m);
15
+ return counter && counter > 1 ? `${m} (×${counter})` : m;
16
+ });
17
+ }
@@ -2,6 +2,7 @@ import { getPartTypeByName, getRegularDocumentTypeByName, getRoutingDocumentType
2
2
  import { createSimpleTranslator, } from "@paroicms/public-server-lib";
3
3
  import { getRandomImagePath } from "../lib/images-lib.js";
4
4
  import { createTaskCollector } from "../lib/tasks.js";
5
+ import { dedupMessages } from "./content-helpers.js";
5
6
  import { createGeneratedContentReport } from "./content-report.js";
6
7
  import { generateLocalizedFooterMention } from "./create-node-contents.js";
7
8
  import { generateFieldSetContent, generateMultipleFieldSetContents, } from "./generate-fake-content.js";
@@ -161,8 +162,9 @@ async function addRegularDocuments(ctx, report, siteOptions, nodeOptions) {
161
162
  tolerateErrors,
162
163
  debugName: nodeType.kebabName,
163
164
  });
164
- if (tolerateErrors.errorMessages.length > 0) {
165
- ctx.logger.warn(`Error generating content for ${nodeType.typeName}:\n - ${tolerateErrors.errorMessages.join("\n - ")}`);
165
+ const errorMessages = dedupMessages(tolerateErrors.errorMessages);
166
+ if (errorMessages.length > 0) {
167
+ ctx.logger.warn(`Error generating content for ${nodeType.typeName}:\n - ${errorMessages.join("\n - ")}`);
166
168
  }
167
169
  await ctx.service.connector.addMultipleDocumentContents(fqdn, {
168
170
  parentNodeId,
@@ -187,8 +189,9 @@ async function addParts(ctx, report, siteOptions, nodeOptions) {
187
189
  tolerateErrors,
188
190
  debugName: nodeType.kebabName,
189
191
  });
190
- if (tolerateErrors.errorMessages.length > 0) {
191
- ctx.logger.warn(`Error generating content for ${nodeType.typeName}:\n - ${tolerateErrors.errorMessages.join("\n - ")}`);
192
+ const errorMessages = dedupMessages(tolerateErrors.errorMessages);
193
+ if (errorMessages.length > 0) {
194
+ ctx.logger.warn(`Error generating content for ${nodeType.typeName}:\n - ${errorMessages.join("\n - ")}`);
192
195
  }
193
196
  await ctx.service.connector.addMultiplePartContents(fqdn, {
194
197
  parentNodeId,
@@ -0,0 +1,33 @@
1
+ import jwt from "jsonwebtoken";
2
+ import { createApiError } from "../errors.js";
3
+ const JWT_SECRET = "init"; // Hardcoded JWT secret as specified
4
+ /**
5
+ * Verifies a session token and returns the session ID if valid
6
+ * @param token The JWT token to verify
7
+ * @returns The session ID contained in the token
8
+ * @throws ApiError if the token is invalid or expired
9
+ */
10
+ export function verifySessionToken(token) {
11
+ try {
12
+ // Verify and decode the token
13
+ const payload = jwt.verify(token, JWT_SECRET);
14
+ if (!payload || !payload.sessionId) {
15
+ throw createApiError("Invalid session token", 401);
16
+ }
17
+ return payload.sessionId;
18
+ }
19
+ catch (error) {
20
+ if (error.name === "TokenExpiredError") {
21
+ throw createApiError("Session token expired", 401);
22
+ }
23
+ else if (error.name === "JsonWebTokenError") {
24
+ throw createApiError("Invalid session token", 401);
25
+ }
26
+ // Re-throw our custom error
27
+ if (error.name === "ApiError") {
28
+ throw error;
29
+ }
30
+ // Unexpected error
31
+ throw createApiError("Session verification failed", 500);
32
+ }
33
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -106,9 +106,9 @@ export function parseLlmRawTags(llmResponse, tagNames, options = {}) {
106
106
  const current = matches[i];
107
107
  if (current.isOpening) {
108
108
  // Find the next corresponding closing tag
109
- let j = i + 1;
109
+ const j = i + 1;
110
110
  let foundClosing = false;
111
- while (j < matches.length) {
111
+ if (j < matches.length) {
112
112
  const next = matches[j];
113
113
  // If we encounter another opening tag of any type before finding our closing tag,
114
114
  // it's an error if not tolerating errors
@@ -119,9 +119,15 @@ export function parseLlmRawTags(llmResponse, tagNames, options = {}) {
119
119
  tolerateErrors.errorMessages.push(message);
120
120
  foundClosing = undefined;
121
121
  // If we are tolerating errors, we skip this opening tag entirely
122
- break;
123
122
  }
124
- if (!next.isOpening && next.tagName === current.tagName) {
123
+ else {
124
+ if (next.tagName !== current.tagName) {
125
+ // Found a non-matching closing tag
126
+ const message = `Mismatched tags: opening <${current.tagName}>, closing </${next.tagName}>`;
127
+ if (!tolerateErrors)
128
+ throw new Error(message);
129
+ tolerateErrors.errorMessages.push(message);
130
+ }
125
131
  // Found a matching closing tag
126
132
  const contentStart = current.position + `<${current.tagName}>`.length;
127
133
  const contentEnd = next.position;
@@ -133,19 +139,7 @@ export function parseLlmRawTags(llmResponse, tagNames, options = {}) {
133
139
  // Skip to after this closing tag
134
140
  i = j;
135
141
  foundClosing = true;
136
- break;
137
142
  }
138
- if (!next.isOpening && next.tagName !== current.tagName) {
139
- // Found a non-matching closing tag
140
- const message = `Mismatched tags: opening <${current.tagName}>, closing </${next.tagName}>`;
141
- if (!tolerateErrors)
142
- throw new Error(message);
143
- tolerateErrors.errorMessages.push(message);
144
- foundClosing = undefined;
145
- // If we are tolerating errors, we skip this current opening tag entirely
146
- break;
147
- }
148
- ++j;
149
143
  }
150
144
  // Handle case where no matching closing tag was found
151
145
  if (foundClosing === false) {
@@ -0,0 +1,118 @@
1
+ // Default limits if not specified in configuration
2
+ const DEFAULT_REQUEST_TOKENS = 8000;
3
+ const DEFAULT_SESSION_TOKENS = 32000;
4
+ const DEFAULT_DAILY_TOKENS = 100000;
5
+ /**
6
+ * Initialize token tracking for a session
7
+ */
8
+ export function initTokenTracking(pluginConf) {
9
+ const requestLimit = pluginConf.maxRequestTokens ?? DEFAULT_REQUEST_TOKENS;
10
+ const sessionLimit = pluginConf.maxSessionTokens ?? DEFAULT_SESSION_TOKENS;
11
+ return {
12
+ requestLimit,
13
+ sessionLimit,
14
+ sessionUsage: 0,
15
+ userLimits: new Map(),
16
+ };
17
+ }
18
+ /**
19
+ * Roughly estimate token count from text length
20
+ * This is a simple approximation - for production, use a proper tokenizer
21
+ */
22
+ export function estimateTokenCount(text) {
23
+ // Rough estimate: 1 token ≈ 4 characters for English text
24
+ return Math.ceil(text.length / 4);
25
+ }
26
+ /**
27
+ * Track token usage for the current user
28
+ */
29
+ export function trackUserTokens(ctx, tokenCount) {
30
+ const { userKey, tokenUsage, logger } = ctx;
31
+ const dailyLimit = ctx.pluginConf.maxDailyTokens ?? DEFAULT_DAILY_TOKENS;
32
+ // Update session usage
33
+ tokenUsage.sessionUsage += tokenCount;
34
+ // Get or create user usage entry
35
+ let userUsage = tokenUsage.userLimits.get(userKey);
36
+ if (!userUsage) {
37
+ userUsage = {
38
+ dailyUsage: 0,
39
+ lastReset: new Date(),
40
+ totalRequests: 0,
41
+ };
42
+ tokenUsage.userLimits.set(userKey, userUsage);
43
+ }
44
+ // Reset daily usage if it's a new day
45
+ const today = new Date().toDateString();
46
+ if (userUsage.lastReset.toDateString() !== today) {
47
+ logger.info(`Resetting daily token usage for user ${userKey}`);
48
+ userUsage.dailyUsage = 0;
49
+ userUsage.lastReset = new Date();
50
+ }
51
+ // Update daily usage
52
+ userUsage.dailyUsage += tokenCount;
53
+ userUsage.totalRequests++;
54
+ // Log usage
55
+ logger.debug(`Token usage - User: ${userKey}, Request: ${tokenCount}, ` +
56
+ `Session: ${tokenUsage.sessionUsage}/${tokenUsage.sessionLimit}, ` +
57
+ `Daily: ${userUsage.dailyUsage}/${dailyLimit}`);
58
+ }
59
+ /**
60
+ * Check if token limits have been exceeded
61
+ * @throws Error if any limit is exceeded
62
+ */
63
+ export function checkTokenLimits(ctx, estimatedTokens) {
64
+ const { tokenUsage, userKey, logger } = ctx;
65
+ const dailyLimit = ctx.pluginConf.maxDailyTokens ?? DEFAULT_DAILY_TOKENS;
66
+ // Check single request limit
67
+ if (estimatedTokens > tokenUsage.requestLimit) {
68
+ logger.warn(`User ${userKey} exceeded request token limit: ${estimatedTokens}/${tokenUsage.requestLimit}`);
69
+ throw new Error(`Prompt exceeds maximum token limit of ${tokenUsage.requestLimit}`);
70
+ }
71
+ // Check session limit
72
+ if (tokenUsage.sessionUsage + estimatedTokens > tokenUsage.sessionLimit) {
73
+ logger.warn(`User ${userKey} exceeded session token limit: ${tokenUsage.sessionUsage}/${tokenUsage.sessionLimit}`);
74
+ throw new Error("Session token limit exceeded. Please start a new session.");
75
+ }
76
+ // Check daily limit
77
+ const userUsage = tokenUsage.userLimits.get(userKey);
78
+ if (userUsage && userUsage.dailyUsage + estimatedTokens > dailyLimit) {
79
+ logger.warn(`User ${userKey} exceeded daily token limit: ${userUsage.dailyUsage}/${dailyLimit}`);
80
+ throw new Error("Daily token limit exceeded. Please try again tomorrow.");
81
+ }
82
+ }
83
+ /**
84
+ * Safe wrapper for LLM invocation with token tracking and limits
85
+ */
86
+ export async function invokeWithTokenLimit(ctx, model, prompt, processor) {
87
+ // For text prompts, estimate tokens
88
+ let promptTokens = 0;
89
+ if (typeof prompt === "string") {
90
+ promptTokens = estimateTokenCount(prompt);
91
+ }
92
+ else if (prompt.message && typeof prompt.message === "string") {
93
+ promptTokens = estimateTokenCount(prompt.message);
94
+ }
95
+ else {
96
+ // JSON objects are harder to estimate; use a conservative approach
97
+ promptTokens = estimateTokenCount(JSON.stringify(prompt)) * 1.5;
98
+ }
99
+ // Check limits before making the call
100
+ checkTokenLimits(ctx, promptTokens);
101
+ // Make the LLM call
102
+ const response = await model.invoke(prompt);
103
+ // Estimate response tokens and track total
104
+ let responseContent = "";
105
+ if (typeof response.content === "string") {
106
+ responseContent = response.content;
107
+ }
108
+ else if (response.content && response.content.length > 0) {
109
+ // Handle array response
110
+ responseContent = response.content.join("\n");
111
+ }
112
+ const responseTokens = estimateTokenCount(responseContent);
113
+ const totalTokens = promptTokens + responseTokens;
114
+ // Track the usage
115
+ trackUserTokens(ctx, totalTokens);
116
+ // Process and return the response
117
+ return processor(response);
118
+ }
@@ -18,13 +18,13 @@ const fieldsPrompt = await createPromptTemplate({
18
18
  export async function invokeNewSiteAnalysis(ctx, input) {
19
19
  const { analysis, explanation, unusedInformation } = await invokeAnalysisStep1(ctx, input);
20
20
  const siteSchema = createSiteSchemaFromAnalysis(analysis);
21
- await invokeAnalysisStep2(ctx, { prompt: unusedInformation ?? "" }, siteSchema);
21
+ const { unusedInformation: unusedInformation2 } = await invokeAnalysisStep2(ctx, { prompt: createUnusedInformationPrompt(unusedInformation, analysis) ?? "" }, siteSchema);
22
22
  reorderSiteSchemaNodeTypes(siteSchema);
23
23
  const l10n = createL10n(analysis, siteSchema);
24
24
  const siteTitle = {
25
25
  [analysis.siteProperties.language]: analysis.siteProperties.title,
26
26
  };
27
- if (!unusedInformation) {
27
+ if (!unusedInformation2) {
28
28
  await updateSession(ctx, { status: "analyzed", promptCountInc: 1 });
29
29
  return {
30
30
  siteTitle,
@@ -34,9 +34,9 @@ export async function invokeNewSiteAnalysis(ctx, input) {
34
34
  explanation,
35
35
  };
36
36
  }
37
- ctx.logger.debug("Unused information:", unusedInformation);
37
+ ctx.logger.debug("Unused information:", unusedInformation2);
38
38
  const updated = await invokeUpdateSiteSchema(ctx, {
39
- prompt: unusedInformation,
39
+ prompt: unusedInformation2,
40
40
  generatedSchema: {
41
41
  siteTitle,
42
42
  siteSchema,
@@ -125,16 +125,23 @@ siteSchema) {
125
125
  const llmMessage = await fieldsPrompt.pipe(ctx.goodModel).invoke(llmInput);
126
126
  llmMessageContent = await debug.getMessageContent(llmMessage);
127
127
  }
128
- const { assignedFields } = parseLlmResponseAsProperties(llmMessageContent, [
128
+ const { assignedFields, unusedInformation } = parseLlmResponseAsProperties(llmMessageContent, [
129
129
  {
130
130
  tagName: "yaml_result",
131
131
  key: "assignedFields",
132
132
  format: "yaml",
133
133
  },
134
+ {
135
+ tagName: "unused_information_md",
136
+ key: "unusedInformation",
137
+ format: "markdown",
138
+ optional: true,
139
+ },
134
140
  ]);
135
141
  if (siteSchema.nodeTypes) {
136
142
  assignFieldsToNodeTypes(ctx, assignedFields, siteSchema.nodeTypes);
137
143
  }
144
+ return { unusedInformation };
138
145
  }
139
146
  function assignFieldsToNodeTypes(ctx, assignedFields, nodeTypes) {
140
147
  const remainingTypeNames = new Set(Object.keys(assignedFields));
@@ -172,3 +179,15 @@ function reorderSiteSchemaNodeTypes(siteSchema) {
172
179
  "orderChildrenBy",
173
180
  ]));
174
181
  }
182
+ function createUnusedInformationPrompt(unusedInformation, analysis) {
183
+ const prompts = Object.entries(analysis.dictionary)
184
+ .map(([typeName, entry]) => {
185
+ return entry.prompt ? `${typeName}: ${entry.prompt}` : undefined;
186
+ })
187
+ .filter(Boolean);
188
+ if (prompts.length > 0) {
189
+ const nodeTypePrompts = `To do:\n\n- ${prompts.join("- \n")}`;
190
+ return unusedInformation ? `${nodeTypePrompts}\n\n${unusedInformation}` : nodeTypePrompts;
191
+ }
192
+ return unusedInformation;
193
+ }
@@ -0,0 +1,33 @@
1
+ import { ApiError } from "@paroicms/public-server-lib";
2
+ import { nanoid } from "nanoid";
3
+ const { sign, verify } = (await import("jsonwebtoken")).default;
4
+ const JWT_SECRET = "init"; // FIXME: Hardcoded JWT secret as specified
5
+ export async function newSession(ctx, command) {
6
+ const { service } = ctx;
7
+ const { recaptchaToken } = command;
8
+ const isValid = await service.validateRecaptchaResponse(recaptchaToken);
9
+ if (!isValid)
10
+ throw new ApiError("Invalid reCAPTCHA token", 400);
11
+ const sessionId = nanoid();
12
+ const token = sign({ sessionId }, JWT_SECRET, { expiresIn: "24h" });
13
+ return { token };
14
+ }
15
+ /**
16
+ * Verifies a session token and returns the session ID if valid
17
+ */
18
+ export function verifySessionToken(token) {
19
+ let payload;
20
+ try {
21
+ payload = verify(token, JWT_SECRET);
22
+ }
23
+ catch (error) {
24
+ if (error.name === "TokenExpiredError")
25
+ throw new ApiError("Session token expired", 401);
26
+ if (error.name === "JsonWebTokenError")
27
+ throw new ApiError("Invalid session token", 401);
28
+ throw error;
29
+ }
30
+ if (!payload || !payload.sessionId)
31
+ throw new ApiError("Invalid session token", 401);
32
+ return payload.sessionId;
33
+ }
@@ -0,0 +1,17 @@
1
+ import { ApiError } from "@paroicms/public-server-lib";
2
+ import { nanoid } from "nanoid";
3
+ const { sign, verify } = (await import("jsonwebtoken")).default;
4
+ export async function newSession(ctx, command) {
5
+ const { service } = ctx;
6
+ const { recaptchaToken } = command;
7
+ const isValid = await service.validateRecaptchaResponse(recaptchaToken);
8
+ if (!isValid) {
9
+ throw new ApiError("Invalid reCAPTCHA token", {
10
+ status: 400,
11
+ });
12
+ }
13
+ const sessionId = nanoid();
14
+ const token = sign({ sessionId }, "init", // Hardcoded JWT secret as specified
15
+ { expiresIn: "1h" });
16
+ return { token };
17
+ }