@ganju/utils 0.0.1

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 (220) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +9 -0
  3. package/README.md +23 -0
  4. package/dist/apiError.d.ts +3 -0
  5. package/dist/apiError.d.ts.map +1 -0
  6. package/dist/apiError.js +36 -0
  7. package/dist/attachment.d.ts +33 -0
  8. package/dist/attachment.d.ts.map +1 -0
  9. package/dist/attachment.js +59 -0
  10. package/dist/base64.d.ts +10 -0
  11. package/dist/base64.d.ts.map +1 -0
  12. package/dist/base64.js +50 -0
  13. package/dist/channelDebounce.d.ts +26 -0
  14. package/dist/channelDebounce.d.ts.map +1 -0
  15. package/dist/channelDebounce.js +35 -0
  16. package/dist/channelNotifier.d.ts +9 -0
  17. package/dist/channelNotifier.d.ts.map +1 -0
  18. package/dist/channelNotifier.js +31 -0
  19. package/dist/chunking.d.ts +28 -0
  20. package/dist/chunking.d.ts.map +1 -0
  21. package/dist/chunking.js +97 -0
  22. package/dist/cliConstants.d.ts +9 -0
  23. package/dist/cliConstants.d.ts.map +1 -0
  24. package/dist/cliConstants.js +47 -0
  25. package/dist/coerce.d.ts +2 -0
  26. package/dist/coerce.d.ts.map +1 -0
  27. package/dist/coerce.js +13 -0
  28. package/dist/constants.d.ts +624 -0
  29. package/dist/constants.d.ts.map +1 -0
  30. package/dist/constants.js +2220 -0
  31. package/dist/crypto.d.ts +14 -0
  32. package/dist/crypto.d.ts.map +1 -0
  33. package/dist/crypto.js +93 -0
  34. package/dist/customCodeProject.d.ts +91 -0
  35. package/dist/customCodeProject.d.ts.map +1 -0
  36. package/dist/customCodeProject.js +154 -0
  37. package/dist/customCodeToken.d.ts +41 -0
  38. package/dist/customCodeToken.d.ts.map +1 -0
  39. package/dist/customCodeToken.js +115 -0
  40. package/dist/deadline.d.ts +6 -0
  41. package/dist/deadline.d.ts.map +1 -0
  42. package/dist/deadline.js +45 -0
  43. package/dist/discordSend.d.ts +25 -0
  44. package/dist/discordSend.d.ts.map +1 -0
  45. package/dist/discordSend.js +8 -0
  46. package/dist/embeddable.d.ts +2 -0
  47. package/dist/embeddable.d.ts.map +1 -0
  48. package/dist/embeddable.js +6 -0
  49. package/dist/exposedResource.d.ts +6 -0
  50. package/dist/exposedResource.d.ts.map +1 -0
  51. package/dist/exposedResource.js +18 -0
  52. package/dist/extractedDocument.d.ts +70 -0
  53. package/dist/extractedDocument.d.ts.map +1 -0
  54. package/dist/extractedDocument.js +2 -0
  55. package/dist/fetcher.d.ts +8 -0
  56. package/dist/fetcher.d.ts.map +1 -0
  57. package/dist/fetcher.js +68 -0
  58. package/dist/formatFilename.d.ts +2 -0
  59. package/dist/formatFilename.d.ts.map +1 -0
  60. package/dist/formatFilename.js +19 -0
  61. package/dist/formatRelative.d.ts +12 -0
  62. package/dist/formatRelative.d.ts.map +1 -0
  63. package/dist/formatRelative.js +28 -0
  64. package/dist/formatRelativeTime.d.ts +2 -0
  65. package/dist/formatRelativeTime.d.ts.map +1 -0
  66. package/dist/formatRelativeTime.js +49 -0
  67. package/dist/getEnv.d.ts +5 -0
  68. package/dist/getEnv.d.ts.map +1 -0
  69. package/dist/getEnv.js +11 -0
  70. package/dist/gmailSend.d.ts +24 -0
  71. package/dist/gmailSend.d.ts.map +1 -0
  72. package/dist/gmailSend.js +9 -0
  73. package/dist/index.d.ts +2839 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +155 -0
  76. package/dist/jsonSchemaToZodShape.d.ts +38 -0
  77. package/dist/jsonSchemaToZodShape.d.ts.map +1 -0
  78. package/dist/jsonSchemaToZodShape.js +117 -0
  79. package/dist/l2Normalize.d.ts +2 -0
  80. package/dist/l2Normalize.d.ts.map +1 -0
  81. package/dist/l2Normalize.js +25 -0
  82. package/dist/languageCookieDomain.d.ts +21 -0
  83. package/dist/languageCookieDomain.d.ts.map +1 -0
  84. package/dist/languageCookieDomain.js +39 -0
  85. package/dist/localizeZodIssue.d.ts +43 -0
  86. package/dist/localizeZodIssue.d.ts.map +1 -0
  87. package/dist/localizeZodIssue.js +90 -0
  88. package/dist/mailHeaders.d.ts +4 -0
  89. package/dist/mailHeaders.d.ts.map +1 -0
  90. package/dist/mailHeaders.js +11 -0
  91. package/dist/mcpProxy.d.ts +2 -0
  92. package/dist/mcpProxy.d.ts.map +1 -0
  93. package/dist/mcpProxy.js +21 -0
  94. package/dist/mimeMessage.d.ts +21 -0
  95. package/dist/mimeMessage.d.ts.map +1 -0
  96. package/dist/mimeMessage.js +58 -0
  97. package/dist/oauth.d.ts +21 -0
  98. package/dist/oauth.d.ts.map +1 -0
  99. package/dist/oauth.js +75 -0
  100. package/dist/oauthProviders.d.ts +12 -0
  101. package/dist/oauthProviders.d.ts.map +1 -0
  102. package/dist/oauthProviders.js +99 -0
  103. package/dist/outlookSend.d.ts +19 -0
  104. package/dist/outlookSend.d.ts.map +1 -0
  105. package/dist/outlookSend.js +9 -0
  106. package/dist/parseHttpError.d.ts +2 -0
  107. package/dist/parseHttpError.d.ts.map +1 -0
  108. package/dist/parseHttpError.js +23 -0
  109. package/dist/planLimitError.d.ts +18 -0
  110. package/dist/planLimitError.d.ts.map +1 -0
  111. package/dist/planLimitError.js +35 -0
  112. package/dist/processQueueBatch.d.ts +17 -0
  113. package/dist/processQueueBatch.d.ts.map +1 -0
  114. package/dist/processQueueBatch.js +32 -0
  115. package/dist/reservedToolName.d.ts +16 -0
  116. package/dist/reservedToolName.d.ts.map +1 -0
  117. package/dist/reservedToolName.js +25 -0
  118. package/dist/resourceUri.d.ts +2 -0
  119. package/dist/resourceUri.d.ts.map +1 -0
  120. package/dist/resourceUri.js +19 -0
  121. package/dist/retry.d.ts +10 -0
  122. package/dist/retry.d.ts.map +1 -0
  123. package/dist/retry.js +37 -0
  124. package/dist/sanitize.d.ts +7 -0
  125. package/dist/sanitize.d.ts.map +1 -0
  126. package/dist/sanitize.js +69 -0
  127. package/dist/schema.d.ts +2175 -0
  128. package/dist/schema.d.ts.map +1 -0
  129. package/dist/schema.js +1382 -0
  130. package/dist/sdkConstants.d.ts +15 -0
  131. package/dist/sdkConstants.d.ts.map +1 -0
  132. package/dist/sdkConstants.js +43 -0
  133. package/dist/slackSend.d.ts +38 -0
  134. package/dist/slackSend.d.ts.map +1 -0
  135. package/dist/slackSend.js +14 -0
  136. package/dist/slug.d.ts +5 -0
  137. package/dist/slug.d.ts.map +1 -0
  138. package/dist/slug.js +34 -0
  139. package/dist/slugifyTitle.d.ts +2 -0
  140. package/dist/slugifyTitle.d.ts.map +1 -0
  141. package/dist/slugifyTitle.js +11 -0
  142. package/dist/sources.d.ts +43 -0
  143. package/dist/sources.d.ts.map +1 -0
  144. package/dist/sources.js +73 -0
  145. package/dist/ssrf.d.ts +3 -0
  146. package/dist/ssrf.d.ts.map +1 -0
  147. package/dist/ssrf.js +46 -0
  148. package/dist/tallyUsageKinds.d.ts +9 -0
  149. package/dist/tallyUsageKinds.d.ts.map +1 -0
  150. package/dist/tallyUsageKinds.js +20 -0
  151. package/dist/telegramSend.d.ts +28 -0
  152. package/dist/telegramSend.d.ts.map +1 -0
  153. package/dist/telegramSend.js +9 -0
  154. package/dist/toolCatalog.d.ts +452 -0
  155. package/dist/toolCatalog.d.ts.map +1 -0
  156. package/dist/toolCatalog.js +552 -0
  157. package/dist/validateMessageVariables.d.ts +7 -0
  158. package/dist/validateMessageVariables.d.ts.map +1 -0
  159. package/dist/validateMessageVariables.js +16 -0
  160. package/dist/vendorError.d.ts +2 -0
  161. package/dist/vendorError.d.ts.map +1 -0
  162. package/dist/vendorError.js +25 -0
  163. package/dist/whatsappSend.d.ts +31 -0
  164. package/dist/whatsappSend.d.ts.map +1 -0
  165. package/dist/whatsappSend.js +11 -0
  166. package/package.json +76 -0
  167. package/src/apiError.ts +40 -0
  168. package/src/attachment.ts +97 -0
  169. package/src/base64.ts +60 -0
  170. package/src/channelDebounce.ts +76 -0
  171. package/src/channelNotifier.ts +39 -0
  172. package/src/chunking.ts +131 -0
  173. package/src/cliConstants.ts +50 -0
  174. package/src/coerce.ts +9 -0
  175. package/src/constants.ts +2542 -0
  176. package/src/crypto.ts +108 -0
  177. package/src/customCodeProject.ts +182 -0
  178. package/src/customCodeToken.ts +198 -0
  179. package/src/deadline.ts +43 -0
  180. package/src/discordSend.ts +41 -0
  181. package/src/embeddable.ts +4 -0
  182. package/src/exposedResource.ts +40 -0
  183. package/src/extractedDocument.ts +62 -0
  184. package/src/fetcher.ts +82 -0
  185. package/src/formatFilename.ts +19 -0
  186. package/src/formatRelative.ts +36 -0
  187. package/src/formatRelativeTime.ts +45 -0
  188. package/src/getEnv.ts +13 -0
  189. package/src/gmailSend.ts +34 -0
  190. package/src/index.ts +397 -0
  191. package/src/jsonSchemaToZodShape.ts +111 -0
  192. package/src/l2Normalize.ts +19 -0
  193. package/src/languageCookieDomain.ts +37 -0
  194. package/src/localizeZodIssue.ts +124 -0
  195. package/src/mailHeaders.ts +12 -0
  196. package/src/mcpProxy.ts +20 -0
  197. package/src/mimeMessage.ts +82 -0
  198. package/src/oauth.ts +103 -0
  199. package/src/oauthProviders.ts +118 -0
  200. package/src/outlookSend.ts +44 -0
  201. package/src/parseHttpError.ts +16 -0
  202. package/src/planLimitError.ts +50 -0
  203. package/src/processQueueBatch.ts +46 -0
  204. package/src/reservedToolName.ts +27 -0
  205. package/src/resourceUri.ts +17 -0
  206. package/src/retry.ts +43 -0
  207. package/src/sanitize.ts +67 -0
  208. package/src/schema.ts +1624 -0
  209. package/src/sdkConstants.ts +44 -0
  210. package/src/slackSend.ts +65 -0
  211. package/src/slug.ts +37 -0
  212. package/src/slugifyTitle.ts +8 -0
  213. package/src/sources.ts +117 -0
  214. package/src/ssrf.ts +34 -0
  215. package/src/tallyUsageKinds.ts +20 -0
  216. package/src/telegramSend.ts +39 -0
  217. package/src/toolCatalog.ts +635 -0
  218. package/src/validateMessageVariables.ts +18 -0
  219. package/src/vendorError.ts +19 -0
  220. package/src/whatsappSend.ts +47 -0
@@ -0,0 +1,16 @@
1
+ export const parseHttpErrorMessage = async (response: Response): Promise<string> => {
2
+ const raw = await response.text();
3
+ if (!raw) return `${response.status} ${response.statusText}`;
4
+
5
+ try {
6
+ const parsed = JSON.parse(raw);
7
+ if (parsed?.error?.message && typeof parsed.error.message === 'string') {
8
+ return parsed.error.message;
9
+ }
10
+ if (typeof parsed?.error === 'string') return parsed.error;
11
+ if (typeof parsed?.message === 'string') return parsed.message;
12
+ return raw;
13
+ } catch {
14
+ return raw;
15
+ }
16
+ };
@@ -0,0 +1,50 @@
1
+ import { constants } from './constants';
2
+
3
+ // Thrown when an organization hits a hard plan quota. It carries an explicit
4
+ // HTTP status (402 Payment Required) and a stable `code` so the API's central
5
+ // error handler can return them verbatim and the dashboard can recognise the
6
+ // block and show an "upgrade" call-to-action instead of a generic error.
7
+ export interface PlanLimitDetails {
8
+ // Which quota was hit — one of constants.PLAN_FEATURE_*.
9
+ feature: string;
10
+ // The org's current effective plan (FREE / PRO / ENTERPRISE).
11
+ plan: string;
12
+ // The numeric limit that was exceeded, when applicable (e.g. 5 tools).
13
+ limit?: number | null;
14
+ // The current usage at the time of the block.
15
+ used?: number | null;
16
+ }
17
+
18
+ export class PlanLimitError extends Error {
19
+ readonly status = 402;
20
+ readonly code = constants.PLAN_LIMIT_ERROR_CODE;
21
+ readonly feature: string;
22
+ readonly plan: string;
23
+ readonly limit: number | null;
24
+ readonly used: number | null;
25
+
26
+ constructor(message: string, details: PlanLimitDetails) {
27
+ super(message);
28
+ this.name = 'PlanLimitError';
29
+ this.feature = details.feature;
30
+ this.plan = details.plan;
31
+ this.limit = details.limit ?? null;
32
+ this.used = details.used ?? null;
33
+ }
34
+
35
+ // The extra fields merged into the JSON error body so clients get structured
36
+ // context alongside the message.
37
+ toBody(): Record<string, unknown> {
38
+ return {
39
+ code: this.code,
40
+ feature: this.feature,
41
+ plan: this.plan,
42
+ limit: this.limit,
43
+ used: this.used,
44
+ upgrade: true
45
+ };
46
+ }
47
+ }
48
+
49
+ export const isPlanLimitError = (error: unknown): error is PlanLimitError =>
50
+ error instanceof PlanLimitError;
@@ -0,0 +1,46 @@
1
+ import { isRateLimitError, isTransientInfraError } from './retry';
2
+ import { constants } from './constants';
3
+
4
+ export interface QueueMessageLike<T> {
5
+ body: T;
6
+ ack: () => void;
7
+ retry: (opts?: { delaySeconds?: number }) => void;
8
+ }
9
+
10
+ export interface QueueBatchLike<T> {
11
+ queue: string;
12
+ messages: readonly QueueMessageLike<T>[];
13
+ }
14
+
15
+ export interface ProcessQueueBatchHandlers<T> {
16
+ process: (body: T) => Promise<void>;
17
+ onError: (error: unknown, body: T, queueName: string) => Promise<void>;
18
+ }
19
+
20
+ export const processQueueBatch = async <T>(
21
+ batch: QueueBatchLike<T>,
22
+ handlers: ProcessQueueBatchHandlers<T>
23
+ ): Promise<void> => {
24
+ for (const message of batch.messages) {
25
+ try {
26
+ await handlers.process(message.body);
27
+ message.ack();
28
+ } catch (error) {
29
+ if (isTransientInfraError(error)) {
30
+ // Transient Cloudflare platform hiccup (e.g. a Durable Object /
31
+ // Container reset). The job itself is fine, so re-run it after a short
32
+ // backoff without reporting an error or marking the work failed.
33
+ message.retry({ delaySeconds: constants.TRANSIENT_BACKOFF_SECONDS });
34
+ continue;
35
+ }
36
+ await handlers.onError(error, message.body, batch.queue);
37
+ if (isRateLimitError(error)) {
38
+ message.retry({
39
+ delaySeconds: constants.RATE_LIMIT_BACKOFF_SECONDS
40
+ });
41
+ } else {
42
+ message.retry();
43
+ }
44
+ }
45
+ }
46
+ };
@@ -0,0 +1,27 @@
1
+ import { constants } from './constants';
2
+
3
+ const RESERVED = new Set(constants.RESERVED_TOOL_NAMES);
4
+
5
+ /**
6
+ * Is this MCP tool name reserved by the platform?
7
+ *
8
+ * Called on the WRITE path only — when a user names an `http-endpoint` tool or
9
+ * uploads a `custom-code` manifest. Deliberately NOT applied when reading a
10
+ * stored row back at boot: tightening this list must never stop an already
11
+ * installed tool from registering, which would be a silent failure the owner
12
+ * could only diagnose from logs. A legacy name that is reserved today keeps
13
+ * working; it simply loses the name to the native tool, because apps/mcp
14
+ * registers natives first.
15
+ *
16
+ * See RESERVED_TOOL_NAME_PREFIXES in constants.ts for why this reserves whole
17
+ * namespaces rather than the enumerated set of shipped keys.
18
+ */
19
+ export const isReservedToolName = (name: string): boolean => {
20
+ const lower = name.toLowerCase();
21
+ return (
22
+ RESERVED.has(lower) ||
23
+ constants.RESERVED_TOOL_NAME_PREFIXES.some(prefix =>
24
+ lower.startsWith(prefix)
25
+ )
26
+ );
27
+ };
@@ -0,0 +1,17 @@
1
+ import { constants } from './constants';
2
+
3
+ // The `resource://…` URI the dashboard generates from a title when the author
4
+ // doesn't type one, and the same one the broker derives for a script-created
5
+ // resource that arrives without a uri.
6
+ //
7
+ // Shared rather than duplicated because the two have to agree: a resource a
8
+ // tool writes should be addressable exactly like one a person uploaded, and a
9
+ // script asked to "replace the Q3 report" has to be able to name the row the
10
+ // dashboard created. Hyphens, not the underscores `slugifyTitle` produces —
11
+ // that one names MCP prompts and bot commands, where an underscore is legal and
12
+ // a hyphen isn't.
13
+ export const resourceUriFromTitle = (title: string): string =>
14
+ `${constants.CUSTOM_CODE_RESOURCE_URI_PREFIX}${title
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9]+/g, '-')
17
+ .replace(/^-+|-+$/g, '')}`;
package/src/retry.ts ADDED
@@ -0,0 +1,43 @@
1
+ export const sleep = (ms: number): Promise<void> =>
2
+ new Promise<void>(resolve => setTimeout(resolve, ms));
3
+
4
+ export const isRateLimitError = (err: unknown): boolean => {
5
+ const msg = String((err as { message?: unknown })?.message ?? err);
6
+ return /\b429\b|quota|RESOURCE_EXHAUSTED|rate.?limit/i.test(msg);
7
+ };
8
+
9
+ // Transient Cloudflare platform errors that are safe to retry as-is: a Durable
10
+ // Object / Container (e.g. the resource-handler) getting evicted or reset
11
+ // mid-request, a code-update eviction, or a dropped internal connection. These
12
+ // are infra hiccups, not job failures, so callers should re-run the job rather
13
+ // than report an error or mark the work failed.
14
+ export const isTransientInfraError = (err: unknown): boolean => {
15
+ const msg = String((err as { message?: unknown })?.message ?? err);
16
+ return /object to be reset|Durable Object reset|code (?:was|has been) updated|cannot access storage|Network connection lost|internal error.*Durable Object|Durable Object.*internal error/i.test(
17
+ msg
18
+ );
19
+ };
20
+
21
+ export interface RateLimitRetryOptions {
22
+ maxAttempts?: number;
23
+ baseDelayMs?: number;
24
+ maxDelayMs?: number;
25
+ }
26
+
27
+ export const withRateLimitRetry = async <T>(
28
+ fn: () => Promise<T>,
29
+ options: RateLimitRetryOptions = {}
30
+ ): Promise<T> => {
31
+ const { maxAttempts = 5, baseDelayMs = 500, maxDelayMs = 10_000 } = options;
32
+ let attempt = 0;
33
+ while (true) {
34
+ try {
35
+ return await fn();
36
+ } catch (err) {
37
+ attempt += 1;
38
+ if (attempt >= maxAttempts || !isRateLimitError(err)) throw err;
39
+ const delay = Math.min(2 ** attempt * baseDelayMs, maxDelayMs);
40
+ await sleep(delay);
41
+ }
42
+ }
43
+ };
@@ -0,0 +1,67 @@
1
+ import { constants } from './constants';
2
+
3
+ export const decodeEntities = (value: string): string =>
4
+ value
5
+ .replace(/&lt;/g, '<')
6
+ .replace(/&gt;/g, '>')
7
+ .replace(/&quot;/g, '"')
8
+ .replace(/&apos;/g, "'")
9
+ .replace(/&amp;/g, '&');
10
+
11
+ // Escape a string for safe interpolation into HTML text or double-quoted
12
+ // attribute values. The `&` replacement must run first so the entities added
13
+ // by the later replacements are not double-escaped.
14
+ export const escapeHtml = (value: string): string =>
15
+ value
16
+ .replace(/&/g, '&amp;')
17
+ .replace(/</g, '&lt;')
18
+ .replace(/>/g, '&gt;')
19
+ .replace(/"/g, '&quot;')
20
+ .replace(/'/g, '&#39;');
21
+
22
+ export const parseOpenXmlProps = (xml: string): Record<string, string> => {
23
+ const out: Record<string, string> = {};
24
+ const re =
25
+ /<(?:[A-Za-z][\w-]*:)?([A-Za-z][\w-]*)(?:\s[^>]*)?>([^<]*)<\/(?:[A-Za-z][\w-]*:)?\1>/g;
26
+ let match: RegExpExecArray | null;
27
+ while ((match = re.exec(xml)) !== null) {
28
+ const value = decodeEntities(match[2]).trim();
29
+ if (value) out[match[1]] = value;
30
+ }
31
+ return out;
32
+ };
33
+
34
+ export const stripBase64Images = (text: string): string =>
35
+ text
36
+ .replace(/<img\b[^>]*\bsrc=["']?data:image\/[^>]*>/gi, '')
37
+ .replace(/!\[[^\]]*\]\(\s*data:image\/[^)]*\)/g, '')
38
+ .replace(constants.BASE64_DATA_URI_RE, '')
39
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '')
40
+ .replace(/[ \t]+\n/g, '\n')
41
+ .replace(/\n{3,}/g, '\n\n')
42
+ .trim();
43
+
44
+ export const sanitizeMetadataString = (value: string): string =>
45
+ value
46
+ .replace(constants.BASE64_DATA_URI_RE, '')
47
+ .replace(constants.RAW_BASE64_BLOB_RE, '')
48
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '')
49
+ .trim();
50
+
51
+ export const serializeMetadataValue = (value: unknown): unknown => {
52
+ if (value instanceof Date) return value.toISOString();
53
+ if (typeof value === 'string') {
54
+ const cleaned = sanitizeMetadataString(value);
55
+ return cleaned || undefined;
56
+ }
57
+ if (Array.isArray(value)) return value.map(serializeMetadataValue);
58
+ if (value && typeof value === 'object') {
59
+ const out: Record<string, unknown> = {};
60
+ for (const [k, v] of Object.entries(value)) {
61
+ const serialized = serializeMetadataValue(v);
62
+ if (serialized !== undefined) out[k] = serialized;
63
+ }
64
+ return out;
65
+ }
66
+ return value;
67
+ };