@naisys/common 3.0.0-beta.4 → 3.0.0-beta.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.
@@ -1,12 +1,12 @@
1
1
  import { sanitizeSpendLimit } from "./configUtils.js";
2
2
  /** Keys that should never be distributed to clients */
3
3
  const EXCLUDED_KEYS = [
4
- "HUB_ACCESS_KEY",
5
- "HUB_PORT",
6
- "NAISYS_FOLDER",
7
- "NAISYS_HOSTNAME",
8
- "NODE_ENV",
9
- "SUPERVISOR_PORT",
4
+ "HUB_ACCESS_KEY",
5
+ "HUB_PORT",
6
+ "NAISYS_FOLDER",
7
+ "NAISYS_HOSTNAME",
8
+ "NODE_ENV",
9
+ "SUPERVISOR_PORT",
10
10
  ];
11
11
  /**
12
12
  * Builds hub-distributable config from the provided env vars.
@@ -15,44 +15,44 @@ const EXCLUDED_KEYS = [
15
15
  * When undefined (e.g. .env fallback), all variables are exported for backwards compat.
16
16
  */
17
17
  export function buildClientConfig(variables, shellExportKeys) {
18
- const shellCommand = {
19
- outputTokenMax: 7500,
20
- timeoutSeconds: 10,
21
- maxTimeoutSeconds: 60 * 5,
22
- };
23
- const retrySecondsMax = 30 * 60;
24
- const webTokenMax = 5000;
25
- const compactSessionEnabled = true;
26
- const preemptiveCompactEnabled = true;
27
- // Build variableMap, filtering out excluded keys and undefined values
28
- const variableMap = {};
29
- const shellVariableMap = {};
30
- for (const [key, value] of Object.entries(variables)) {
31
- if (value !== undefined && !EXCLUDED_KEYS.includes(key)) {
32
- variableMap[key] = value;
33
- // When shellExportKeys is undefined (standalone .env mode), export all
34
- if (!shellExportKeys || shellExportKeys.has(key)) {
35
- shellVariableMap[key] = value;
36
- }
37
- }
18
+ const shellCommand = {
19
+ outputTokenMax: 7500,
20
+ timeoutSeconds: 10,
21
+ maxTimeoutSeconds: 60 * 5,
22
+ };
23
+ const retrySecondsMax = 30 * 60;
24
+ const webTokenMax = 5000;
25
+ const compactSessionEnabled = true;
26
+ const preemptiveCompactEnabled = true;
27
+ // Build variableMap, filtering out excluded keys and undefined values
28
+ const variableMap = {};
29
+ const shellVariableMap = {};
30
+ for (const [key, value] of Object.entries(variables)) {
31
+ if (value !== undefined && !EXCLUDED_KEYS.includes(key)) {
32
+ variableMap[key] = value;
33
+ // When shellExportKeys is undefined (standalone .env mode), export all
34
+ if (!shellExportKeys || shellExportKeys.has(key)) {
35
+ shellVariableMap[key] = value;
36
+ }
38
37
  }
39
- const googleSearchEngineId = variableMap.GOOGLE_SEARCH_ENGINE_ID;
40
- const spendLimitDollars = sanitizeSpendLimit(variableMap.SPEND_LIMIT_DOLLARS);
41
- const spendLimitHours = sanitizeSpendLimit(variableMap.SPEND_LIMIT_HOURS);
42
- const useToolsForLlmConsoleResponses = true;
43
- const autoStartAgentsOnMessage = true;
44
- return {
45
- shellCommand,
46
- retrySecondsMax,
47
- webTokenMax,
48
- compactSessionEnabled,
49
- preemptiveCompactEnabled,
50
- variableMap,
51
- shellVariableMap,
52
- googleSearchEngineId,
53
- spendLimitDollars,
54
- spendLimitHours,
55
- useToolsForLlmConsoleResponses,
56
- autoStartAgentsOnMessage,
57
- };
38
+ }
39
+ const googleSearchEngineId = variableMap.GOOGLE_SEARCH_ENGINE_ID;
40
+ const spendLimitDollars = sanitizeSpendLimit(variableMap.SPEND_LIMIT_DOLLARS);
41
+ const spendLimitHours = sanitizeSpendLimit(variableMap.SPEND_LIMIT_HOURS);
42
+ const useToolsForLlmConsoleResponses = true;
43
+ const autoStartAgentsOnMessage = true;
44
+ return {
45
+ shellCommand,
46
+ retrySecondsMax,
47
+ webTokenMax,
48
+ compactSessionEnabled,
49
+ preemptiveCompactEnabled,
50
+ variableMap,
51
+ shellVariableMap,
52
+ googleSearchEngineId,
53
+ spendLimitDollars,
54
+ spendLimitHours,
55
+ useToolsForLlmConsoleResponses,
56
+ autoStartAgentsOnMessage,
57
+ };
58
58
  }
@@ -1,42 +1,42 @@
1
1
  import { z } from "zod/v4";
2
2
  export const HateoasLinkSchema = z.object({
3
- rel: z.string(),
4
- href: z.string(),
5
- method: z.string().optional(),
6
- title: z.string().optional(),
7
- schema: z.string().optional(),
3
+ rel: z.string(),
4
+ href: z.string(),
5
+ method: z.string().optional(),
6
+ title: z.string().optional(),
7
+ schema: z.string().optional(),
8
8
  });
9
9
  export const AlternateEncodingSchema = z.object({
10
- contentType: z.string(),
11
- description: z.string().optional(),
12
- fileFields: z.array(z.string()),
10
+ contentType: z.string(),
11
+ description: z.string().optional(),
12
+ fileFields: z.array(z.string()),
13
13
  });
14
14
  export const HateoasActionSchema = z.object({
15
- rel: z.string(),
16
- href: z.string(),
17
- method: z.string(),
18
- title: z.string().optional(),
19
- schema: z.string().optional(),
20
- body: z.record(z.string(), z.unknown()).optional(),
21
- alternateEncoding: AlternateEncodingSchema.optional(),
22
- disabled: z.boolean().optional(),
23
- disabledReason: z.union([z.string(), z.array(z.string())]).optional(),
15
+ rel: z.string(),
16
+ href: z.string(),
17
+ method: z.string(),
18
+ title: z.string().optional(),
19
+ schema: z.string().optional(),
20
+ body: z.record(z.string(), z.unknown()).optional(),
21
+ alternateEncoding: AlternateEncodingSchema.optional(),
22
+ disabled: z.boolean().optional(),
23
+ disabledReason: z.union([z.string(), z.array(z.string())]).optional(),
24
24
  });
25
25
  export const HateoasActionTemplateSchema = z.object({
26
- rel: z.string(),
27
- hrefTemplate: z.string(),
28
- method: z.string(),
29
- title: z.string().optional(),
30
- schema: z.string().optional(),
31
- body: z.record(z.string(), z.unknown()).optional(),
32
- alternateEncoding: AlternateEncodingSchema.optional(),
26
+ rel: z.string(),
27
+ hrefTemplate: z.string(),
28
+ method: z.string(),
29
+ title: z.string().optional(),
30
+ schema: z.string().optional(),
31
+ body: z.record(z.string(), z.unknown()).optional(),
32
+ alternateEncoding: AlternateEncodingSchema.optional(),
33
33
  });
34
34
  export const HateoasLinkTemplateSchema = z.object({
35
- rel: z.string(),
36
- hrefTemplate: z.string(),
37
- title: z.string().optional(),
35
+ rel: z.string(),
36
+ hrefTemplate: z.string(),
37
+ title: z.string().optional(),
38
38
  });
39
39
  export const HateoasLinksSchema = z.object({
40
- _links: z.array(HateoasLinkSchema),
41
- _actions: z.array(HateoasActionSchema).optional(),
40
+ _links: z.array(HateoasLinkSchema),
41
+ _actions: z.array(HateoasActionSchema).optional(),
42
42
  });
package/dist/hateoas.js CHANGED
@@ -4,63 +4,59 @@
4
4
  * (e.g. when rendering a disabled button with a tooltip).
5
5
  */
6
6
  export function hasAction(actions, rel, opts) {
7
- const a = actions?.find((a) => a.rel === rel);
8
- if (!a)
9
- return undefined;
10
- if (a.disabled && !opts?.includeDisabled)
11
- return undefined;
12
- return a;
7
+ const a = actions?.find((a) => a.rel === rel);
8
+ if (!a) return undefined;
9
+ if (a.disabled && !opts?.includeDisabled) return undefined;
10
+ return a;
13
11
  }
14
12
  export function hasActionTemplate(templates, rel) {
15
- return templates?.find((t) => t.rel === rel);
13
+ return templates?.find((t) => t.rel === rel);
16
14
  }
17
15
  export function hasLinkTemplate(templates, rel) {
18
- return templates?.find((t) => t.rel === rel);
16
+ return templates?.find((t) => t.rel === rel);
19
17
  }
20
18
  export function resolveActions(defs, baseHref, ctx, checkPermission) {
21
- const actions = [];
22
- for (const def of defs) {
23
- if (def.statuses) {
24
- const status = ctx.status;
25
- if (!status || !def.statuses.includes(status))
26
- continue;
27
- }
28
- if (def.visibleWhen && !def.visibleWhen(ctx))
29
- continue;
30
- const hasPerm = !def.permission || checkPermission(def.permission);
31
- if (def.hideWithoutPermission && !hasPerm)
32
- continue;
33
- const gate = !hasPerm && def.permission
34
- ? {
35
- disabled: true,
36
- disabledReason: `Requires ${def.permission} permission`,
37
- }
38
- : {};
39
- const disabledReason = hasPerm && def.disabledWhen ? def.disabledWhen(ctx) : null;
40
- actions.push({
41
- rel: def.rel,
42
- href: def.href ?? baseHref + (def.path ?? ""),
43
- method: def.method,
44
- title: def.title,
45
- ...(def.schema ? { schema: def.schema } : {}),
46
- ...(def.body ? { body: def.body } : {}),
47
- ...gate,
48
- ...(disabledReason ? { disabled: true, disabledReason } : {}),
49
- });
19
+ const actions = [];
20
+ for (const def of defs) {
21
+ if (def.statuses) {
22
+ const status = ctx.status;
23
+ if (!status || !def.statuses.includes(status)) continue;
50
24
  }
51
- return actions;
25
+ if (def.visibleWhen && !def.visibleWhen(ctx)) continue;
26
+ const hasPerm = !def.permission || checkPermission(def.permission);
27
+ if (def.hideWithoutPermission && !hasPerm) continue;
28
+ const gate =
29
+ !hasPerm && def.permission
30
+ ? {
31
+ disabled: true,
32
+ disabledReason: `Requires ${def.permission} permission`,
33
+ }
34
+ : {};
35
+ const disabledReason =
36
+ hasPerm && def.disabledWhen ? def.disabledWhen(ctx) : null;
37
+ actions.push({
38
+ rel: def.rel,
39
+ href: def.href ?? baseHref + (def.path ?? ""),
40
+ method: def.method,
41
+ title: def.title,
42
+ ...(def.schema ? { schema: def.schema } : {}),
43
+ ...(def.body ? { body: def.body } : {}),
44
+ ...gate,
45
+ ...(disabledReason ? { disabled: true, disabledReason } : {}),
46
+ });
47
+ }
48
+ return actions;
52
49
  }
53
50
  export function permGate(hasPerm, permission) {
54
- return hasPerm
55
- ? {}
56
- : {
57
- disabled: true,
58
- disabledReason: `Requires ${permission} permission`,
59
- };
51
+ return hasPerm
52
+ ? {}
53
+ : {
54
+ disabled: true,
55
+ disabledReason: `Requires ${permission} permission`,
56
+ };
60
57
  }
61
58
  /** Normalize a `disabledReason` (string | string[] | undefined) to a single display string. */
62
59
  export function formatDisabledReason(reason) {
63
- if (!reason)
64
- return undefined;
65
- return Array.isArray(reason) ? reason.join("\n") : reason;
60
+ if (!reason) return undefined;
61
+ return Array.isArray(reason) ? reason.join("\n") : reason;
66
62
  }
@@ -7,13 +7,16 @@
7
7
  * Interface is duck-typed so @naisys/common doesn't need a Fastify dependency.
8
8
  */
9
9
  export function registerLenientJsonParser(fastify) {
10
- fastify.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
11
- try {
12
- done(null, body.length > 0 ? JSON.parse(body) : {});
13
- }
14
- catch (err) {
15
- err.statusCode = 400;
16
- done(err, undefined);
17
- }
18
- });
10
+ fastify.addContentTypeParser(
11
+ "application/json",
12
+ { parseAs: "string" },
13
+ (_req, body, done) => {
14
+ try {
15
+ done(null, body.length > 0 ? JSON.parse(body) : {});
16
+ } catch (err) {
17
+ err.statusCode = 400;
18
+ done(err, undefined);
19
+ }
20
+ },
21
+ );
19
22
  }
package/dist/mimeTypes.js CHANGED
@@ -1,25 +1,25 @@
1
1
  const MIME_TYPES = {
2
- // Images
3
- ".jpg": "image/jpeg",
4
- ".jpeg": "image/jpeg",
5
- ".png": "image/png",
6
- ".gif": "image/gif",
7
- ".webp": "image/webp",
8
- ".svg": "image/svg+xml",
9
- ".bmp": "image/bmp",
10
- // Audio
11
- ".wav": "audio/wav",
12
- ".mp3": "audio/mpeg",
13
- ".m4a": "audio/mp4",
14
- ".flac": "audio/flac",
15
- ".ogg": "audio/ogg",
16
- ".webm": "audio/webm",
17
- // Documents
18
- ".pdf": "application/pdf",
19
- ".txt": "text/plain",
20
- ".json": "application/json",
2
+ // Images
3
+ ".jpg": "image/jpeg",
4
+ ".jpeg": "image/jpeg",
5
+ ".png": "image/png",
6
+ ".gif": "image/gif",
7
+ ".webp": "image/webp",
8
+ ".svg": "image/svg+xml",
9
+ ".bmp": "image/bmp",
10
+ // Audio
11
+ ".wav": "audio/wav",
12
+ ".mp3": "audio/mpeg",
13
+ ".m4a": "audio/mp4",
14
+ ".flac": "audio/flac",
15
+ ".ogg": "audio/ogg",
16
+ ".webm": "audio/webm",
17
+ // Documents
18
+ ".pdf": "application/pdf",
19
+ ".txt": "text/plain",
20
+ ".json": "application/json",
21
21
  };
22
22
  export function mimeFromFilename(filename) {
23
- const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
24
- return MIME_TYPES[ext] ?? "application/octet-stream";
23
+ const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
24
+ return MIME_TYPES[ext] ?? "application/octet-stream";
25
25
  }
@@ -3,15 +3,15 @@ import { builtInImageModels, builtInLlmModels } from "./builtInModels.js";
3
3
  // --- Enums ---
4
4
  export var LlmApiType;
5
5
  (function (LlmApiType) {
6
- LlmApiType["OpenAI"] = "openai";
7
- LlmApiType["Google"] = "google";
8
- LlmApiType["Anthropic"] = "anthropic";
9
- LlmApiType["Mock"] = "mock";
10
- LlmApiType["None"] = "none";
6
+ LlmApiType["OpenAI"] = "openai";
7
+ LlmApiType["Google"] = "google";
8
+ LlmApiType["Anthropic"] = "anthropic";
9
+ LlmApiType["Mock"] = "mock";
10
+ LlmApiType["None"] = "none";
11
11
  })(LlmApiType || (LlmApiType = {}));
12
12
  // --- Model schemas ---
13
13
  export const LlmModelSchema = z
14
- .object({
14
+ .object({
15
15
  key: z.string().min(1),
16
16
  label: z.string().min(1),
17
17
  versionName: z.string().min(1),
@@ -27,116 +27,119 @@ export const LlmModelSchema = z
27
27
  supportsVision: z.boolean().optional(),
28
28
  supportsHearing: z.boolean().optional(),
29
29
  supportsComputerUse: z.boolean().optional(),
30
- })
31
- .superRefine((data, ctx) => {
32
- if (data.baseUrl &&
33
- ![LlmApiType.OpenAI, LlmApiType.Anthropic, LlmApiType.Google].includes(data.apiType)) {
34
- ctx.addIssue({
35
- code: z.ZodIssueCode.custom,
36
- message: `baseUrl is only supported for OpenAI, Anthropic, and Google API types (got "${data.apiType}")`,
37
- path: ["baseUrl"],
38
- });
30
+ })
31
+ .superRefine((data, ctx) => {
32
+ if (
33
+ data.baseUrl &&
34
+ ![LlmApiType.OpenAI, LlmApiType.Anthropic, LlmApiType.Google].includes(
35
+ data.apiType,
36
+ )
37
+ ) {
38
+ ctx.addIssue({
39
+ code: z.ZodIssueCode.custom,
40
+ message: `baseUrl is only supported for OpenAI, Anthropic, and Google API types (got "${data.apiType}")`,
41
+ path: ["baseUrl"],
42
+ });
39
43
  }
40
- });
44
+ });
41
45
  export const ImageModelSchema = z.object({
42
- key: z.string().min(1),
43
- label: z.string().min(1),
44
- versionName: z.string().min(1),
45
- size: z.string().min(1),
46
- baseUrl: z.string().optional(),
47
- apiKeyVar: z.string(),
48
- cost: z.number(),
49
- quality: z.enum(["standard", "hd", "high", "medium", "low"]).optional(),
46
+ key: z.string().min(1),
47
+ label: z.string().min(1),
48
+ versionName: z.string().min(1),
49
+ size: z.string().min(1),
50
+ baseUrl: z.string().optional(),
51
+ apiKeyVar: z.string(),
52
+ cost: z.number(),
53
+ quality: z.enum(["standard", "hd", "high", "medium", "low"]).optional(),
50
54
  });
51
55
  // --- Custom models file schema ---
52
56
  export const CustomModelsFileSchema = z.object({
53
- llmModels: z.array(LlmModelSchema).optional(),
54
- imageModels: z.array(ImageModelSchema).optional(),
57
+ llmModels: z.array(LlmModelSchema).optional(),
58
+ imageModels: z.array(ImageModelSchema).optional(),
55
59
  });
56
60
  // --- DB meta schemas (for JSON stored in models.meta column) ---
57
61
  const LlmMetaSchema = z.object({
58
- apiType: z.enum(LlmApiType),
59
- maxTokens: z.number().int().positive(),
60
- baseUrl: z.string().optional(),
61
- apiKeyVar: z.string(),
62
- inputCost: z.number().default(0),
63
- outputCost: z.number().default(0),
64
- cacheWriteCost: z.number().optional(),
65
- cacheReadCost: z.number().optional(),
66
- cacheTtlSeconds: z.number().int().positive().optional(),
67
- supportsVision: z.boolean().optional(),
68
- supportsHearing: z.boolean().optional(),
69
- supportsComputerUse: z.boolean().optional(),
62
+ apiType: z.enum(LlmApiType),
63
+ maxTokens: z.number().int().positive(),
64
+ baseUrl: z.string().optional(),
65
+ apiKeyVar: z.string(),
66
+ inputCost: z.number().default(0),
67
+ outputCost: z.number().default(0),
68
+ cacheWriteCost: z.number().optional(),
69
+ cacheReadCost: z.number().optional(),
70
+ cacheTtlSeconds: z.number().int().positive().optional(),
71
+ supportsVision: z.boolean().optional(),
72
+ supportsHearing: z.boolean().optional(),
73
+ supportsComputerUse: z.boolean().optional(),
70
74
  });
71
75
  const ImageMetaSchema = z.object({
72
- size: z.string().min(1),
73
- baseUrl: z.string().optional(),
74
- apiKeyVar: z.string(),
75
- cost: z.number(),
76
- quality: z.enum(["standard", "hd", "high", "medium", "low"]).optional(),
76
+ size: z.string().min(1),
77
+ baseUrl: z.string().optional(),
78
+ apiKeyVar: z.string(),
79
+ cost: z.number(),
80
+ quality: z.enum(["standard", "hd", "high", "medium", "low"]).optional(),
77
81
  });
78
82
  export function llmModelToDbFields(model, isBuiltin, isCustom) {
79
- const { key, label, versionName, ...metaFields } = model;
80
- return {
81
- key,
82
- type: "llm",
83
- label,
84
- version_name: versionName,
85
- is_builtin: isBuiltin,
86
- is_custom: isCustom,
87
- meta: JSON.stringify(metaFields),
88
- };
83
+ const { key, label, versionName, ...metaFields } = model;
84
+ return {
85
+ key,
86
+ type: "llm",
87
+ label,
88
+ version_name: versionName,
89
+ is_builtin: isBuiltin,
90
+ is_custom: isCustom,
91
+ meta: JSON.stringify(metaFields),
92
+ };
89
93
  }
90
94
  export function imageModelToDbFields(model, isBuiltin, isCustom) {
91
- const { key, label, versionName, ...metaFields } = model;
92
- return {
93
- key,
94
- type: "image",
95
- label,
96
- version_name: versionName,
97
- is_builtin: isBuiltin,
98
- is_custom: isCustom,
99
- meta: JSON.stringify(metaFields),
100
- };
95
+ const { key, label, versionName, ...metaFields } = model;
96
+ return {
97
+ key,
98
+ type: "image",
99
+ label,
100
+ version_name: versionName,
101
+ is_builtin: isBuiltin,
102
+ is_custom: isCustom,
103
+ meta: JSON.stringify(metaFields),
104
+ };
101
105
  }
102
106
  export function dbFieldsToLlmModel(row) {
103
- const meta = LlmMetaSchema.parse(JSON.parse(row.meta));
104
- return {
105
- key: row.key,
106
- label: row.label,
107
- versionName: row.version_name,
108
- ...meta,
109
- };
107
+ const meta = LlmMetaSchema.parse(JSON.parse(row.meta));
108
+ return {
109
+ key: row.key,
110
+ label: row.label,
111
+ versionName: row.version_name,
112
+ ...meta,
113
+ };
110
114
  }
111
115
  export function dbFieldsToImageModel(row) {
112
- const meta = ImageMetaSchema.parse(JSON.parse(row.meta));
113
- return {
114
- key: row.key,
115
- label: row.label,
116
- versionName: row.version_name,
117
- ...meta,
118
- };
116
+ const meta = ImageMetaSchema.parse(JSON.parse(row.meta));
117
+ return {
118
+ key: row.key,
119
+ label: row.label,
120
+ versionName: row.version_name,
121
+ ...meta,
122
+ };
119
123
  }
120
124
  // --- Merge helpers ---
121
125
  function mergeModels(builtIn, custom) {
122
- if (!custom || custom.length === 0) {
123
- return [...builtIn];
124
- }
125
- const result = [...builtIn];
126
- for (const c of custom) {
127
- const idx = result.findIndex((m) => m.key === c.key);
128
- if (idx >= 0) {
129
- result[idx] = c;
130
- }
131
- else {
132
- result.push(c);
133
- }
126
+ if (!custom || custom.length === 0) {
127
+ return [...builtIn];
128
+ }
129
+ const result = [...builtIn];
130
+ for (const c of custom) {
131
+ const idx = result.findIndex((m) => m.key === c.key);
132
+ if (idx >= 0) {
133
+ result[idx] = c;
134
+ } else {
135
+ result.push(c);
134
136
  }
135
- return result;
137
+ }
138
+ return result;
136
139
  }
137
140
  export function getAllLlmModels(customLlmModels) {
138
- return mergeModels(builtInLlmModels, customLlmModels);
141
+ return mergeModels(builtInLlmModels, customLlmModels);
139
142
  }
140
143
  export function getAllImageModels(customImageModels) {
141
- return mergeModels(builtInImageModels, customImageModels);
144
+ return mergeModels(builtInImageModels, customImageModels);
142
145
  }
@@ -4,13 +4,19 @@
4
4
  * Interfaces are duck-typed so @naisys/common doesn't need a Fastify dependency.
5
5
  */
6
6
  export function registerSecurityHeaders(fastify, options) {
7
- fastify.addHook("onSend", (_request, reply, _payload, done) => {
8
- reply.header("X-Content-Type-Options", "nosniff");
9
- reply.header("X-Frame-Options", "DENY");
10
- reply.header("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' ws: wss:; font-src 'self' data:; frame-ancestors 'none'");
11
- if (options.enforceHsts) {
12
- reply.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
13
- }
14
- done();
15
- });
7
+ fastify.addHook("onSend", (_request, reply, _payload, done) => {
8
+ reply.header("X-Content-Type-Options", "nosniff");
9
+ reply.header("X-Frame-Options", "DENY");
10
+ reply.header(
11
+ "Content-Security-Policy",
12
+ "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' ws: wss:; font-src 'self' data:; frame-ancestors 'none'",
13
+ );
14
+ if (options.enforceHsts) {
15
+ reply.header(
16
+ "Strict-Transport-Security",
17
+ "max-age=31536000; includeSubDomains",
18
+ );
19
+ }
20
+ done();
21
+ });
16
22
  }
package/dist/sleep.js CHANGED
@@ -1,4 +1,4 @@
1
1
  /** Async sleep utility. Usage: `await sleep(1000)` */
2
2
  export function sleep(ms) {
3
- return new Promise((resolve) => setTimeout(resolve, ms));
3
+ return new Promise((resolve) => setTimeout(resolve, ms));
4
4
  }
@@ -1,17 +1,20 @@
1
1
  /** Characters allowed in URL path segments used as database keys (usernames, hostnames). */
2
2
  export const URL_SAFE_KEY_REGEX = /^[a-zA-Z0-9_-]+$/;
3
- export const URL_SAFE_KEY_MESSAGE = "Must contain only letters, numbers, hyphens, and underscores";
3
+ export const URL_SAFE_KEY_MESSAGE =
4
+ "Must contain only letters, numbers, hyphens, and underscores";
4
5
  /** Sanitize a string into a URL-safe key (replace spaces/special chars with hyphens). */
5
6
  export function toUrlSafeKey(input) {
6
- return input
7
- .trim()
8
- .replace(/[^a-zA-Z0-9_-]/g, "-")
9
- .replace(/-{2,}/g, "-")
10
- .replace(/^-+|-+$/g, "");
7
+ return input
8
+ .trim()
9
+ .replace(/[^a-zA-Z0-9_-]/g, "-")
10
+ .replace(/-{2,}/g, "-")
11
+ .replace(/^-+|-+$/g, "");
11
12
  }
12
13
  /** Throws if the value is not a valid URL-safe key. */
13
14
  export function assertUrlSafeKey(value, label) {
14
- if (!URL_SAFE_KEY_REGEX.test(value)) {
15
- throw new Error(`${label} "${value}" is not URL-safe. ${URL_SAFE_KEY_MESSAGE}`);
16
- }
15
+ if (!URL_SAFE_KEY_REGEX.test(value)) {
16
+ throw new Error(
17
+ `${label} "${value}" is not URL-safe. ${URL_SAFE_KEY_MESSAGE}`,
18
+ );
19
+ }
17
20
  }