silgi 0.0.14 → 0.1.0-beta.10

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 (210) hide show
  1. package/README.md +102 -1
  2. package/dist/_virtual/_rolldown/runtime.mjs +5 -0
  3. package/dist/adapters/_fetch-adapter.d.mts +18 -0
  4. package/dist/adapters/_fetch-adapter.mjs +53 -0
  5. package/dist/adapters/astro.d.mts +15 -0
  6. package/dist/adapters/astro.mjs +31 -0
  7. package/dist/adapters/aws-lambda.d.mts +42 -0
  8. package/dist/adapters/aws-lambda.mjs +92 -0
  9. package/dist/adapters/express.d.mts +16 -0
  10. package/dist/adapters/express.mjs +120 -0
  11. package/dist/adapters/message-port.d.mts +42 -0
  12. package/dist/adapters/message-port.mjs +132 -0
  13. package/dist/adapters/nestjs.d.mts +25 -0
  14. package/dist/adapters/nestjs.mjs +75 -0
  15. package/dist/adapters/nextjs.d.mts +14 -0
  16. package/dist/adapters/nextjs.mjs +29 -0
  17. package/dist/adapters/peer.d.mts +27 -0
  18. package/dist/adapters/peer.mjs +36 -0
  19. package/dist/adapters/remix.d.mts +15 -0
  20. package/dist/adapters/remix.mjs +30 -0
  21. package/dist/adapters/solidstart.d.mts +12 -0
  22. package/dist/adapters/solidstart.mjs +29 -0
  23. package/dist/adapters/sveltekit.d.mts +14 -0
  24. package/dist/adapters/sveltekit.mjs +30 -0
  25. package/dist/broker/index.d.mts +62 -0
  26. package/dist/broker/index.mjs +153 -0
  27. package/dist/broker/nats.d.mts +33 -0
  28. package/dist/broker/nats.mjs +31 -0
  29. package/dist/broker/redis.d.mts +51 -0
  30. package/dist/broker/redis.mjs +92 -0
  31. package/dist/builder.d.mts +55 -0
  32. package/dist/builder.mjs +70 -0
  33. package/dist/callable.d.mts +19 -0
  34. package/dist/callable.mjs +42 -0
  35. package/dist/caller.mjs +90 -0
  36. package/dist/client/adapters/fetch/index.d.mts +19 -0
  37. package/dist/client/adapters/fetch/index.mjs +82 -0
  38. package/dist/client/adapters/ofetch/index.d.mts +57 -0
  39. package/dist/client/adapters/ofetch/index.mjs +100 -0
  40. package/dist/client/adapters/websocket/index.d.mts +20 -0
  41. package/dist/client/adapters/websocket/index.mjs +101 -0
  42. package/dist/client/client.d.mts +37 -0
  43. package/dist/client/client.mjs +80 -0
  44. package/dist/client/consume.d.mts +50 -0
  45. package/dist/client/consume.mjs +66 -0
  46. package/dist/client/dynamic-link.d.mts +16 -0
  47. package/dist/client/dynamic-link.mjs +19 -0
  48. package/dist/client/index.d.mts +6 -0
  49. package/dist/client/index.mjs +5 -0
  50. package/dist/client/interceptor.d.mts +31 -0
  51. package/dist/client/interceptor.mjs +34 -0
  52. package/dist/client/openapi.d.mts +29 -0
  53. package/dist/client/openapi.mjs +89 -0
  54. package/dist/client/plugins/batch.d.mts +26 -0
  55. package/dist/client/plugins/batch.mjs +64 -0
  56. package/dist/client/plugins/circuit-breaker.d.mts +24 -0
  57. package/dist/client/plugins/circuit-breaker.mjs +60 -0
  58. package/dist/client/plugins/csrf.d.mts +13 -0
  59. package/dist/client/plugins/csrf.mjs +20 -0
  60. package/dist/client/plugins/dedupe.d.mts +10 -0
  61. package/dist/client/plugins/dedupe.mjs +28 -0
  62. package/dist/client/plugins/index.d.mts +8 -0
  63. package/dist/client/plugins/index.mjs +8 -0
  64. package/dist/client/plugins/otel.d.mts +12 -0
  65. package/dist/client/plugins/otel.mjs +27 -0
  66. package/dist/client/plugins/retry.d.mts +34 -0
  67. package/dist/client/plugins/retry.mjs +79 -0
  68. package/dist/client/plugins/timeout.d.mts +10 -0
  69. package/dist/client/plugins/timeout.mjs +14 -0
  70. package/dist/client/server.d.mts +16 -0
  71. package/dist/client/server.mjs +62 -0
  72. package/dist/client/types.d.mts +29 -0
  73. package/dist/codec/devalue.d.mts +21 -0
  74. package/dist/codec/devalue.mjs +33 -0
  75. package/dist/codec/msgpack.d.mts +18 -0
  76. package/dist/codec/msgpack.mjs +45 -0
  77. package/dist/codec/sanitize.mjs +38 -0
  78. package/dist/compile.d.mts +46 -0
  79. package/dist/compile.mjs +332 -0
  80. package/dist/contract.d.mts +36 -0
  81. package/dist/contract.mjs +40 -0
  82. package/dist/core/codec.mjs +67 -0
  83. package/dist/core/dispatch.mjs +62 -0
  84. package/dist/core/error.d.mts +104 -0
  85. package/dist/core/error.mjs +139 -0
  86. package/dist/core/handler.d.mts +6 -0
  87. package/dist/core/handler.mjs +148 -0
  88. package/dist/core/input.mjs +49 -0
  89. package/dist/core/iterator.d.mts +17 -0
  90. package/dist/core/iterator.mjs +79 -0
  91. package/dist/core/router-utils.d.mts +25 -0
  92. package/dist/core/router-utils.mjs +98 -0
  93. package/dist/core/schema.d.mts +20 -0
  94. package/dist/core/schema.mjs +33 -0
  95. package/dist/core/serve.d.mts +51 -0
  96. package/dist/core/serve.mjs +76 -0
  97. package/dist/core/sse.d.mts +18 -0
  98. package/dist/core/sse.mjs +98 -0
  99. package/dist/core/storage.d.mts +20 -0
  100. package/dist/core/storage.mjs +61 -0
  101. package/dist/core/task.d.mts +62 -0
  102. package/dist/core/task.mjs +165 -0
  103. package/dist/core/utils.mjs +24 -0
  104. package/dist/index.d.mts +18 -37
  105. package/dist/index.mjs +15 -7
  106. package/dist/integrations/ai/index.d.mts +25 -0
  107. package/dist/integrations/ai/index.mjs +117 -0
  108. package/dist/integrations/better-auth/index.d.mts +61 -0
  109. package/dist/integrations/better-auth/index.mjs +332 -0
  110. package/dist/integrations/drizzle/index.d.mts +27 -0
  111. package/dist/integrations/drizzle/index.mjs +286 -0
  112. package/dist/integrations/hey-api/index.d.mts +2 -0
  113. package/dist/integrations/hey-api/index.mjs +2 -0
  114. package/dist/integrations/hey-api/to-client.d.mts +20 -0
  115. package/dist/integrations/hey-api/to-client.mjs +39 -0
  116. package/dist/integrations/pinia-colada/general-utils.d.mts +13 -0
  117. package/dist/integrations/pinia-colada/general-utils.mjs +9 -0
  118. package/dist/integrations/pinia-colada/index.d.mts +6 -0
  119. package/dist/integrations/pinia-colada/index.mjs +5 -0
  120. package/dist/integrations/pinia-colada/key.d.mts +11 -0
  121. package/dist/integrations/pinia-colada/key.mjs +11 -0
  122. package/dist/integrations/pinia-colada/procedure-utils.d.mts +25 -0
  123. package/dist/integrations/pinia-colada/procedure-utils.mjs +33 -0
  124. package/dist/integrations/pinia-colada/router-utils.d.mts +17 -0
  125. package/dist/integrations/pinia-colada/router-utils.mjs +30 -0
  126. package/dist/integrations/pinia-colada/types.d.mts +25 -0
  127. package/dist/integrations/react/index.d.mts +83 -0
  128. package/dist/integrations/react/index.mjs +196 -0
  129. package/dist/integrations/tanstack-query/index.d.mts +120 -0
  130. package/dist/integrations/tanstack-query/index.mjs +100 -0
  131. package/dist/integrations/tanstack-query/ssr.d.mts +60 -0
  132. package/dist/integrations/tanstack-query/ssr.mjs +102 -0
  133. package/dist/integrations/zod/converter.d.mts +75 -0
  134. package/dist/integrations/zod/converter.mjs +345 -0
  135. package/dist/integrations/zod/index.d.mts +2 -0
  136. package/dist/integrations/zod/index.mjs +2 -0
  137. package/dist/lazy.d.mts +22 -0
  138. package/dist/lazy.mjs +34 -0
  139. package/dist/lifecycle.d.mts +36 -0
  140. package/dist/lifecycle.mjs +46 -0
  141. package/dist/map-input.d.mts +17 -0
  142. package/dist/map-input.mjs +47 -0
  143. package/dist/plugins/analytics.d.mts +236 -0
  144. package/dist/plugins/analytics.mjs +816 -0
  145. package/dist/plugins/batch-server.d.mts +20 -0
  146. package/dist/plugins/batch-server.mjs +91 -0
  147. package/dist/plugins/body-limit.d.mts +19 -0
  148. package/dist/plugins/body-limit.mjs +49 -0
  149. package/dist/plugins/cache.d.mts +170 -0
  150. package/dist/plugins/cache.mjs +212 -0
  151. package/dist/plugins/coerce.d.mts +24 -0
  152. package/dist/plugins/coerce.mjs +70 -0
  153. package/dist/plugins/cookies.d.mts +14 -0
  154. package/dist/plugins/cookies.mjs +48 -0
  155. package/dist/plugins/cors.d.mts +43 -0
  156. package/dist/plugins/cors.mjs +62 -0
  157. package/dist/plugins/file-upload.d.mts +38 -0
  158. package/dist/plugins/file-upload.mjs +102 -0
  159. package/dist/plugins/index.d.mts +14 -0
  160. package/dist/plugins/index.mjs +14 -0
  161. package/dist/plugins/otel.d.mts +35 -0
  162. package/dist/plugins/otel.mjs +40 -0
  163. package/dist/plugins/pino.d.mts +60 -0
  164. package/dist/plugins/pino.mjs +42 -0
  165. package/dist/plugins/pubsub.d.mts +50 -0
  166. package/dist/plugins/pubsub.mjs +53 -0
  167. package/dist/plugins/ratelimit.d.mts +53 -0
  168. package/dist/plugins/ratelimit.mjs +92 -0
  169. package/dist/plugins/signing.d.mts +41 -0
  170. package/dist/plugins/signing.mjs +118 -0
  171. package/dist/plugins/strict-get.d.mts +10 -0
  172. package/dist/plugins/strict-get.mjs +33 -0
  173. package/dist/scalar.d.mts +49 -0
  174. package/dist/scalar.mjs +311 -0
  175. package/dist/silgi.d.mts +144 -0
  176. package/dist/silgi.mjs +156 -0
  177. package/dist/trpc-interop.d.mts +22 -0
  178. package/dist/trpc-interop.mjs +68 -0
  179. package/dist/types.d.mts +108 -0
  180. package/dist/ws.d.mts +88 -0
  181. package/dist/ws.mjs +205 -0
  182. package/lib/dashboard/index.html +123 -0
  183. package/lib/ocache.d.mts +1 -0
  184. package/lib/ocache.mjs +1 -0
  185. package/lib/ofetch.d.mts +1 -0
  186. package/lib/ofetch.mjs +1 -0
  187. package/lib/srvx.d.mts +1 -0
  188. package/lib/srvx.mjs +1 -0
  189. package/lib/unstorage.d.mts +1 -0
  190. package/lib/unstorage.mjs +1 -0
  191. package/package.json +324 -65
  192. package/bin/silgi.mjs +0 -3
  193. package/dist/chunks/generate.mjs +0 -933
  194. package/dist/chunks/init.mjs +0 -21
  195. package/dist/cli/config.d.mts +0 -19
  196. package/dist/cli/config.d.ts +0 -19
  197. package/dist/cli/config.mjs +0 -5
  198. package/dist/cli/index.d.mts +0 -2
  199. package/dist/cli/index.d.ts +0 -2
  200. package/dist/cli/index.mjs +0 -119
  201. package/dist/index.d.ts +0 -37
  202. package/dist/plugins/openapi.d.mts +0 -138
  203. package/dist/plugins/openapi.d.ts +0 -138
  204. package/dist/plugins/openapi.mjs +0 -204
  205. package/dist/plugins/scalar.d.mts +0 -14
  206. package/dist/plugins/scalar.d.ts +0 -14
  207. package/dist/plugins/scalar.mjs +0 -66
  208. package/dist/shared/silgi.BMCYk2cR.mjs +0 -841
  209. package/dist/shared/silgi.D5qK9QOm.d.mts +0 -301
  210. package/dist/shared/silgi.D5qK9QOm.d.ts +0 -301
@@ -0,0 +1,92 @@
1
+ import { SilgiError } from "../core/error.mjs";
2
+ //#region src/plugins/ratelimit.ts
3
+ /**
4
+ * Rate limiting plugin — v2 guard middleware.
5
+ *
6
+ * Sliding window in-memory rate limiter.
7
+ * Pluggable: swap MemoryRateLimiter for Redis/Upstash/etc.
8
+ */
9
+ var MemoryRateLimiter = class {
10
+ #limit;
11
+ #windowMs;
12
+ #store = /* @__PURE__ */ new Map();
13
+ #evictTimer;
14
+ constructor(options) {
15
+ this.#limit = options.limit;
16
+ this.#windowMs = options.windowMs;
17
+ this.#evictTimer = setInterval(() => this.evict(), this.#windowMs * 2);
18
+ if (typeof this.#evictTimer === "object" && "unref" in this.#evictTimer) this.#evictTimer.unref();
19
+ }
20
+ async limit(key) {
21
+ const now = Date.now();
22
+ const windowStart = now - this.#windowMs;
23
+ let timestamps = this.#store.get(key);
24
+ if (!timestamps) {
25
+ timestamps = [];
26
+ this.#store.set(key, timestamps);
27
+ }
28
+ while (timestamps.length > 0 && timestamps[0] < windowStart) timestamps.shift();
29
+ const remaining = Math.max(0, this.#limit - timestamps.length);
30
+ const reset = timestamps.length > 0 ? timestamps[0] + this.#windowMs : now + this.#windowMs;
31
+ if (timestamps.length >= this.#limit) return {
32
+ success: false,
33
+ limit: this.#limit,
34
+ remaining: 0,
35
+ reset
36
+ };
37
+ timestamps.push(now);
38
+ return {
39
+ success: true,
40
+ limit: this.#limit,
41
+ remaining: remaining - 1,
42
+ reset
43
+ };
44
+ }
45
+ /** Remove entries with no active timestamps to prevent unbounded Map growth */
46
+ evict() {
47
+ const now = Date.now();
48
+ for (const [key, timestamps] of this.#store) {
49
+ while (timestamps.length > 0 && timestamps[0] < now - this.#windowMs) timestamps.shift();
50
+ if (timestamps.length === 0) this.#store.delete(key);
51
+ }
52
+ }
53
+ };
54
+ /**
55
+ * Create a rate limiting guard.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * import { rateLimitGuard, MemoryRateLimiter } from "silgi/ratelimit"
60
+ *
61
+ * const rateLimit = rateLimitGuard({
62
+ * limiter: new MemoryRateLimiter({ limit: 100, windowMs: 60_000 }),
63
+ * keyFn: (ctx) => (ctx as any).ip ?? "anonymous",
64
+ * })
65
+ *
66
+ * const proc = k
67
+ * .$use(rateLimit)
68
+ * .$resolve(() => ({ ok: true }))
69
+ * ```
70
+ */
71
+ function rateLimitGuard(options) {
72
+ return {
73
+ kind: "guard",
74
+ fn: async (ctx) => {
75
+ const key = await options.keyFn(ctx);
76
+ const result = await options.limiter.limit(key);
77
+ if (!result.success) throw new SilgiError("TOO_MANY_REQUESTS", {
78
+ status: 429,
79
+ message: options.message ?? "Rate limit exceeded",
80
+ data: {
81
+ limit: result.limit,
82
+ remaining: result.remaining,
83
+ reset: result.reset,
84
+ retryAfter: Math.ceil((result.reset - Date.now()) / 1e3)
85
+ }
86
+ });
87
+ return { rateLimit: result };
88
+ }
89
+ };
90
+ }
91
+ //#endregion
92
+ export { MemoryRateLimiter, rateLimitGuard };
@@ -0,0 +1,41 @@
1
+ //#region src/plugins/signing.d.ts
2
+ /**
3
+ * Signing & Encryption utilities — HMAC-SHA256 and AES-GCM.
4
+ *
5
+ * Uses the Web Crypto API (works in Node.js, Bun, Deno, Cloudflare Workers).
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { sign, unsign, encrypt, decrypt } from "silgi/plugins"
10
+ *
11
+ * // Sign a value (tamper-proof)
12
+ * const signed = await sign("user:123", "my-secret")
13
+ * const value = await unsign(signed, "my-secret") // "user:123" or null
14
+ *
15
+ * // Encrypt a value (confidential)
16
+ * const encrypted = await encrypt("sensitive-data", "my-secret")
17
+ * const decrypted = await decrypt(encrypted, "my-secret") // "sensitive-data"
18
+ * ```
19
+ */
20
+ /**
21
+ * Sign a string value with HMAC-SHA256.
22
+ * Returns `value.signature` — use `unsign()` to verify.
23
+ */
24
+ declare function sign(value: string, secret: string): Promise<string>;
25
+ /**
26
+ * Verify and extract a signed value.
27
+ * Returns the original value if valid, or `null` if tampered.
28
+ */
29
+ declare function unsign(signed: string, secret: string): Promise<string | null>;
30
+ /**
31
+ * Encrypt a string with AES-256-GCM (PBKDF2 key derivation).
32
+ * Returns a base64url-encoded string containing salt + iv + ciphertext.
33
+ */
34
+ declare function encrypt(plaintext: string, secret: string): Promise<string>;
35
+ /**
36
+ * Decrypt a string encrypted with `encrypt()`.
37
+ * Returns the original plaintext, or throws if the secret is wrong.
38
+ */
39
+ declare function decrypt(encrypted: string, secret: string): Promise<string>;
40
+ //#endregion
41
+ export { decrypt, encrypt, sign, unsign };
@@ -0,0 +1,118 @@
1
+ //#region src/plugins/signing.ts
2
+ /**
3
+ * Signing & Encryption utilities — HMAC-SHA256 and AES-GCM.
4
+ *
5
+ * Uses the Web Crypto API (works in Node.js, Bun, Deno, Cloudflare Workers).
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { sign, unsign, encrypt, decrypt } from "silgi/plugins"
10
+ *
11
+ * // Sign a value (tamper-proof)
12
+ * const signed = await sign("user:123", "my-secret")
13
+ * const value = await unsign(signed, "my-secret") // "user:123" or null
14
+ *
15
+ * // Encrypt a value (confidential)
16
+ * const encrypted = await encrypt("sensitive-data", "my-secret")
17
+ * const decrypted = await decrypt(encrypted, "my-secret") // "sensitive-data"
18
+ * ```
19
+ */
20
+ const encoder = new TextEncoder();
21
+ const decoder = new TextDecoder();
22
+ async function getSigningKey(secret) {
23
+ return crypto.subtle.importKey("raw", encoder.encode(secret), {
24
+ name: "HMAC",
25
+ hash: "SHA-256"
26
+ }, false, ["sign", "verify"]);
27
+ }
28
+ function toHex(buffer) {
29
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
30
+ }
31
+ function fromHex(hex) {
32
+ if (hex.length % 2 !== 0 || hex.length === 0) return null;
33
+ if (!/^[0-9a-f]+$/i.test(hex)) return null;
34
+ const bytes = new Uint8Array(hex.length / 2);
35
+ for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = Number.parseInt(hex.slice(i, i + 2), 16);
36
+ return bytes;
37
+ }
38
+ /**
39
+ * Sign a string value with HMAC-SHA256.
40
+ * Returns `value.signature` — use `unsign()` to verify.
41
+ */
42
+ async function sign(value, secret) {
43
+ const key = await getSigningKey(secret);
44
+ return `${value}.${toHex(await crypto.subtle.sign("HMAC", key, encoder.encode(value)))}`;
45
+ }
46
+ /**
47
+ * Verify and extract a signed value.
48
+ * Returns the original value if valid, or `null` if tampered.
49
+ */
50
+ async function unsign(signed, secret) {
51
+ const dotIdx = signed.lastIndexOf(".");
52
+ if (dotIdx === -1) return null;
53
+ const value = signed.slice(0, dotIdx);
54
+ const expected = fromHex(signed.slice(dotIdx + 1));
55
+ if (!expected) return null;
56
+ const key = await getSigningKey(secret);
57
+ return await crypto.subtle.verify("HMAC", key, expected.buffer, encoder.encode(value)) ? value : null;
58
+ }
59
+ async function getEncryptionKey(secret, salt) {
60
+ const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(secret), "PBKDF2", false, ["deriveKey"]);
61
+ return crypto.subtle.deriveKey({
62
+ name: "PBKDF2",
63
+ salt: salt.buffer,
64
+ iterations: 1e5,
65
+ hash: "SHA-256"
66
+ }, keyMaterial, {
67
+ name: "AES-GCM",
68
+ length: 256
69
+ }, false, ["encrypt", "decrypt"]);
70
+ }
71
+ /**
72
+ * Encrypt a string with AES-256-GCM (PBKDF2 key derivation).
73
+ * Returns a base64url-encoded string containing salt + iv + ciphertext.
74
+ */
75
+ async function encrypt(plaintext, secret) {
76
+ const salt = crypto.getRandomValues(new Uint8Array(16));
77
+ const iv = crypto.getRandomValues(new Uint8Array(12));
78
+ const key = await getEncryptionKey(secret, salt);
79
+ const ciphertext = await crypto.subtle.encrypt({
80
+ name: "AES-GCM",
81
+ iv
82
+ }, key, encoder.encode(plaintext));
83
+ const combined = new Uint8Array(salt.length + iv.length + ciphertext.byteLength);
84
+ combined.set(salt, 0);
85
+ combined.set(iv, salt.length);
86
+ combined.set(new Uint8Array(ciphertext), salt.length + iv.length);
87
+ return base64urlEncode(combined);
88
+ }
89
+ /**
90
+ * Decrypt a string encrypted with `encrypt()`.
91
+ * Returns the original plaintext, or throws if the secret is wrong.
92
+ */
93
+ async function decrypt(encrypted, secret) {
94
+ const combined = base64urlDecode(encrypted);
95
+ const salt = combined.slice(0, 16);
96
+ const iv = combined.slice(16, 28);
97
+ const ciphertext = combined.slice(28);
98
+ const key = await getEncryptionKey(secret, salt);
99
+ const plaintext = await crypto.subtle.decrypt({
100
+ name: "AES-GCM",
101
+ iv
102
+ }, key, ciphertext);
103
+ return decoder.decode(plaintext);
104
+ }
105
+ function base64urlEncode(data) {
106
+ let binary = "";
107
+ const chunkSize = 8192;
108
+ for (let i = 0; i < data.length; i += chunkSize) binary += String.fromCharCode(...data.subarray(i, i + chunkSize));
109
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
110
+ }
111
+ function base64urlDecode(str) {
112
+ const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
113
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
114
+ const binary = atob(padded);
115
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
116
+ }
117
+ //#endregion
118
+ export { decrypt, encrypt, sign, unsign };
@@ -0,0 +1,10 @@
1
+ import { GuardDef } from "../types.mjs";
2
+
3
+ //#region src/plugins/strict-get.d.ts
4
+ /**
5
+ * Guard that rejects non-GET requests. Use on query procedures
6
+ * to enforce RESTful method semantics and prevent CSRF.
7
+ */
8
+ declare const strictGetGuard: GuardDef<Record<string, unknown>>;
9
+ //#endregion
10
+ export { strictGetGuard };
@@ -0,0 +1,33 @@
1
+ import { SilgiError } from "../core/error.mjs";
2
+ //#region src/plugins/strict-get.ts
3
+ /**
4
+ * Strict GET method guard — enforce GET for query procedures.
5
+ *
6
+ * Rejects non-GET requests to query procedures with 405 Method Not Allowed.
7
+ * Mutations must use POST. This prevents CSRF on read endpoints.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { strictGetGuard } from "silgi/plugins"
12
+ *
13
+ * const listUsers = k
14
+ * .$use(strictGetGuard)
15
+ * .$resolve(({ ctx }) => ctx.db.users.findMany())
16
+ * ```
17
+ */
18
+ /**
19
+ * Guard that rejects non-GET requests. Use on query procedures
20
+ * to enforce RESTful method semantics and prevent CSRF.
21
+ */
22
+ const strictGetGuard = {
23
+ kind: "guard",
24
+ fn: (ctx) => {
25
+ const method = ctx.method;
26
+ if (method && method !== "GET" && method !== "HEAD") throw new SilgiError("METHOD_NOT_ALLOWED", {
27
+ status: 405,
28
+ message: `Expected GET, received ${method}`
29
+ });
30
+ }
31
+ };
32
+ //#endregion
33
+ export { strictGetGuard };
@@ -0,0 +1,49 @@
1
+ import { RouterDef } from "./types.mjs";
2
+ //#region src/scalar.d.ts
3
+ interface ScalarOptions {
4
+ title?: string;
5
+ version?: string;
6
+ description?: string;
7
+ servers?: {
8
+ url: string;
9
+ description?: string;
10
+ }[];
11
+ /** Security scheme (e.g. Bearer token) */
12
+ security?: {
13
+ type: 'http' | 'apiKey';
14
+ scheme?: string;
15
+ bearerFormat?: string;
16
+ in?: 'header' | 'query';
17
+ name?: string;
18
+ description?: string;
19
+ };
20
+ /** Contact info */
21
+ contact?: {
22
+ name?: string;
23
+ url?: string;
24
+ email?: string;
25
+ };
26
+ /** License */
27
+ license?: {
28
+ name: string;
29
+ url?: string;
30
+ };
31
+ /** External docs */
32
+ externalDocs?: {
33
+ url: string;
34
+ description?: string;
35
+ };
36
+ /**
37
+ * Scalar UI script source.
38
+ *
39
+ * - `'cdn'` (default) — loads from cdn.jsdelivr.net
40
+ * - `'unpkg'` — loads from unpkg.com
41
+ * - `'local'` — serves from node_modules (offline, requires `@scalar/api-reference` installed)
42
+ * - Custom URL string — self-hosted or local path (e.g. `'/assets/scalar.js'`)
43
+ */
44
+ cdn?: 'cdn' | 'unpkg' | 'local' | (string & {});
45
+ }
46
+ declare function generateOpenAPI(router: RouterDef, options?: ScalarOptions): Record<string, unknown>;
47
+ declare function scalarHTML(specUrl: string, options?: ScalarOptions): string;
48
+ //#endregion
49
+ export { ScalarOptions, generateOpenAPI, scalarHTML };
@@ -0,0 +1,311 @@
1
+ //#region src/scalar.ts
2
+ /**
3
+ * Generate OpenAPI 3.1.0 document from a v2 RouterDef.
4
+ */
5
+ /**
6
+ * Convert `:param` and `:param(regex)` to OpenAPI `{param}` syntax.
7
+ * Returns the converted path and an array of extracted param names.
8
+ */
9
+ function toOpenAPIPath(raw) {
10
+ const pathParams = [];
11
+ const httpPath = raw.replace(/:(\w+)\([^)]*\)/g, (_m, name) => {
12
+ pathParams.push(name);
13
+ return `{${name}}`;
14
+ }).replace(/:(\w+)\?/g, (_m, name) => {
15
+ pathParams.push(name);
16
+ return `{${name}}`;
17
+ }).replace(/:(\w+)/g, (_m, name) => {
18
+ pathParams.push(name);
19
+ return `{${name}}`;
20
+ }).replace(/\/\*\*$/g, "/{path}").replace(/\/\*\*/g, "/{path}");
21
+ if (httpPath.includes("{path}") && !pathParams.includes("path")) pathParams.push("path");
22
+ return {
23
+ httpPath,
24
+ pathParams
25
+ };
26
+ }
27
+ function generateOpenAPI(router, options = {}) {
28
+ const paths = {};
29
+ const tags = /* @__PURE__ */ new Map();
30
+ collectProcedures(router, [], (path, proc) => {
31
+ const route = proc.route;
32
+ const { httpPath, pathParams } = toOpenAPIPath(route?.path ?? "/" + path.join("/"));
33
+ const rawMethod = route?.method?.toLowerCase() ?? "post";
34
+ const methods = rawMethod === "*" ? [
35
+ "get",
36
+ "put",
37
+ "post",
38
+ "delete",
39
+ "options",
40
+ "head",
41
+ "patch",
42
+ "trace"
43
+ ] : [rawMethod];
44
+ const baseOperationId = route?.operationId ?? path.join("_");
45
+ const opTags = route?.tags ?? (path.length > 1 ? [path[0]] : void 0);
46
+ if (opTags) {
47
+ for (const t of opTags) if (!tags.has(t)) tags.set(t, {});
48
+ }
49
+ let description = route?.description;
50
+ if (route?.ws) {
51
+ const wsNote = "Also available over WebSocket (`ws://`). Send `{ id, path: \"" + path.join("/") + "\", input }` as JSON.";
52
+ description = description ? `${description}\n\n${wsNote}` : wsNote;
53
+ }
54
+ const operation = {
55
+ operationId: baseOperationId,
56
+ tags: opTags,
57
+ summary: route?.summary,
58
+ description,
59
+ deprecated: route?.deprecated || void 0,
60
+ responses: {}
61
+ };
62
+ if (!operation.summary) delete operation.summary;
63
+ if (!operation.description) delete operation.description;
64
+ if (!operation.deprecated) delete operation.deprecated;
65
+ if (!operation.tags) delete operation.tags;
66
+ if (route?.security === false) operation.security = [];
67
+ else if (route?.security) operation.security = route.security.map((s) => ({ [s]: [] }));
68
+ else if (options.security) operation.security = [{ auth: [] }];
69
+ const inputSchema = proc.input ? schemaToJsonSchema(proc.input) : null;
70
+ const successStatus = route?.successStatus ?? 200;
71
+ const successDesc = route?.successDescription ?? "Successful response";
72
+ const guards = (proc.use ?? []).filter((m) => m.kind === "guard" && m.errors);
73
+ let allErrors = proc.errors ? { ...proc.errors } : null;
74
+ for (const guard of guards) {
75
+ const ge = guard.errors;
76
+ if (ge) allErrors = allErrors ? {
77
+ ...allErrors,
78
+ ...ge
79
+ } : { ...ge };
80
+ }
81
+ paths[httpPath] ??= {};
82
+ for (const method of methods) {
83
+ const op = {
84
+ ...operation,
85
+ responses: {}
86
+ };
87
+ if (methods.length > 1) op.operationId = `${baseOperationId}_${method}`;
88
+ const params = [];
89
+ for (const p of pathParams) params.push({
90
+ name: p,
91
+ in: "path",
92
+ required: true,
93
+ schema: { type: "string" }
94
+ });
95
+ if (inputSchema) if (method === "get") params.push(...objectSchemaToParams(inputSchema));
96
+ else op.requestBody = {
97
+ required: true,
98
+ content: { "application/json": { schema: inputSchema } }
99
+ };
100
+ if (params.length > 0) op.parameters = params;
101
+ if (proc.type === "subscription") {
102
+ const outputSchema = proc.output ? schemaToJsonSchema(proc.output) : { type: "string" };
103
+ op.responses[String(successStatus)] = {
104
+ description: "SSE event stream",
105
+ content: { "text/event-stream": { schema: {
106
+ type: "string",
107
+ description: `Each line: data: ${JSON.stringify(outputSchema)}`
108
+ } } }
109
+ };
110
+ } else if (proc.output) op.responses[String(successStatus)] = {
111
+ description: successDesc,
112
+ content: { "application/json": { schema: schemaToJsonSchema(proc.output) } }
113
+ };
114
+ else op.responses[String(successStatus)] = { description: successDesc };
115
+ if (proc.input) op.responses["400"] = {
116
+ description: "BAD_REQUEST — input validation failed",
117
+ content: { "application/json": { schema: {
118
+ type: "object",
119
+ properties: {
120
+ code: {
121
+ const: "BAD_REQUEST",
122
+ type: "string"
123
+ },
124
+ status: {
125
+ const: 400,
126
+ type: "integer"
127
+ },
128
+ message: { type: "string" },
129
+ data: {
130
+ type: "object",
131
+ properties: { issues: { type: "array" } }
132
+ }
133
+ },
134
+ required: [
135
+ "code",
136
+ "status",
137
+ "message"
138
+ ]
139
+ } } }
140
+ };
141
+ if (allErrors) {
142
+ const byStatus = /* @__PURE__ */ new Map();
143
+ for (const [code, def] of Object.entries(allErrors)) {
144
+ const status = typeof def === "number" ? def : def.status;
145
+ if (!byStatus.has(status)) byStatus.set(status, []);
146
+ const entry = { code };
147
+ if (typeof def === "object") {
148
+ if (def.message) entry.message = def.message;
149
+ if (def.data) entry.schema = schemaToJsonSchema(def.data);
150
+ }
151
+ byStatus.get(status).push(entry);
152
+ }
153
+ for (const [status, errors] of byStatus) {
154
+ const errorSchemas = errors.map((e) => {
155
+ const s = {
156
+ type: "object",
157
+ properties: {
158
+ code: {
159
+ const: e.code,
160
+ type: "string"
161
+ },
162
+ status: {
163
+ const: status,
164
+ type: "integer"
165
+ },
166
+ message: {
167
+ type: "string",
168
+ ...e.message ? { default: e.message } : {}
169
+ }
170
+ },
171
+ required: [
172
+ "code",
173
+ "status",
174
+ "message"
175
+ ]
176
+ };
177
+ if (e.schema) {
178
+ s.properties.data = e.schema;
179
+ s.required.push("data");
180
+ }
181
+ return s;
182
+ });
183
+ op.responses[String(status)] = {
184
+ description: errors.map((e) => e.code).join(" | "),
185
+ content: { "application/json": { schema: errorSchemas.length === 1 ? errorSchemas[0] : { oneOf: errorSchemas } } }
186
+ };
187
+ }
188
+ }
189
+ let finalOp = op;
190
+ if (route?.spec) if (typeof route.spec === "function") finalOp = route.spec(finalOp);
191
+ else finalOp = {
192
+ ...finalOp,
193
+ ...route.spec
194
+ };
195
+ paths[httpPath][method] = finalOp;
196
+ }
197
+ });
198
+ const doc = {
199
+ openapi: "3.1.0",
200
+ info: {
201
+ title: options.title ?? "Silgi API",
202
+ version: options.version ?? "1.0.0",
203
+ ...options.description ? { description: options.description } : {},
204
+ ...options.contact ? { contact: options.contact } : {},
205
+ ...options.license ? { license: options.license } : {}
206
+ },
207
+ paths
208
+ };
209
+ if (options.servers?.length) doc.servers = options.servers;
210
+ if (options.externalDocs) doc.externalDocs = options.externalDocs;
211
+ if (tags.size > 0) doc.tags = [...tags.entries()].map(([name, meta]) => ({
212
+ name,
213
+ ...meta.description ? { description: meta.description } : {}
214
+ }));
215
+ if (options.security) {
216
+ const scheme = { type: options.security.type };
217
+ if (options.security.type === "http") {
218
+ scheme.scheme = options.security.scheme ?? "bearer";
219
+ if (options.security.bearerFormat) scheme.bearerFormat = options.security.bearerFormat;
220
+ } else if (options.security.type === "apiKey") {
221
+ scheme.in = options.security.in ?? "header";
222
+ scheme.name = options.security.name ?? "x-api-key";
223
+ }
224
+ if (options.security.description) scheme.description = options.security.description;
225
+ doc.components = { securitySchemes: { auth: scheme } };
226
+ }
227
+ return doc;
228
+ }
229
+ const SCALAR_CDN_SOURCES = {
230
+ cdn: "https://cdn.jsdelivr.net/npm/@scalar/api-reference",
231
+ unpkg: "https://unpkg.com/@scalar/api-reference",
232
+ local: "/__silgi/scalar.js"
233
+ };
234
+ function scalarHTML(specUrl, options = {}) {
235
+ const title = escapeHtml(options.title ?? "Silgi API");
236
+ const safeUrl = escapeHtml(specUrl);
237
+ const cdnOption = options.cdn ?? "cdn";
238
+ return `<!DOCTYPE html>
239
+ <html>
240
+ <head>
241
+ <title>${title} — Scalar</title>
242
+ <meta charset="utf-8" />
243
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
244
+ </head>
245
+ <body>
246
+ <script id="api-reference" data-url="${safeUrl}"><\/script>
247
+ <script src="${escapeHtml(SCALAR_CDN_SOURCES[cdnOption] ?? cdnOption)}"><\/script>
248
+ </body>
249
+ </html>`;
250
+ }
251
+ function escapeHtml(s) {
252
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
253
+ }
254
+ function isProcedureDef(value) {
255
+ return typeof value === "object" && value !== null && "type" in value && "resolve" in value && typeof value.resolve === "function";
256
+ }
257
+ function collectProcedures(node, path, cb) {
258
+ if (isProcedureDef(node)) {
259
+ cb(path, node);
260
+ return;
261
+ }
262
+ if (typeof node === "object" && node !== null) for (const [key, child] of Object.entries(node)) collectProcedures(child, [...path, key], cb);
263
+ }
264
+ /**
265
+ * Convert a Standard Schema to JSON Schema via `~standard.jsonSchema.input()`.
266
+ *
267
+ * Works with Zod v4, Valibot, ArkType — any validator implementing Standard Schema v1.
268
+ */
269
+ function schemaToJsonSchema(schema) {
270
+ const std = schema["~standard"];
271
+ if (!std?.jsonSchema?.input) return {};
272
+ try {
273
+ const result = std.jsonSchema.input({ target: "draft-2020-12" });
274
+ if (result && typeof result === "object") {
275
+ const { $schema: _, ...rest } = result;
276
+ return rest;
277
+ }
278
+ } catch {}
279
+ return {};
280
+ }
281
+ function objectSchemaToParams(schema) {
282
+ if (schema.type !== "object" || !schema.properties) return [];
283
+ const required = new Set(schema.required ?? []);
284
+ return Object.entries(schema.properties).map(([name, propSchema]) => ({
285
+ name,
286
+ in: "query",
287
+ required: required.has(name),
288
+ schema: propSchema,
289
+ ...propSchema.description ? { description: propSchema.description } : {}
290
+ }));
291
+ }
292
+ /**
293
+ * Wrap a fetch handler to serve Scalar API Reference at /reference and /openapi.json.
294
+ * Scalar routes are intercepted before the handler — zero overhead for normal requests.
295
+ */
296
+ function wrapWithScalar(handler, routerDef, options = {}) {
297
+ const specJson = JSON.stringify(generateOpenAPI(routerDef, options));
298
+ const specHtml = scalarHTML("/openapi.json", options);
299
+ return (request) => {
300
+ const url = request.url;
301
+ const pathStart = url.indexOf("/", url.indexOf("//") + 2);
302
+ const qMark = url.indexOf("?", pathStart);
303
+ const fullPath = qMark === -1 ? url.slice(pathStart) : url.slice(pathStart, qMark);
304
+ const pathname = fullPath.length > 1 ? fullPath.slice(1) : "";
305
+ if (pathname === "openapi.json") return new Response(specJson, { headers: { "content-type": "application/json" } });
306
+ if (pathname === "reference") return new Response(specHtml, { headers: { "content-type": "text/html" } });
307
+ return handler(request);
308
+ };
309
+ }
310
+ //#endregion
311
+ export { generateOpenAPI, scalarHTML, wrapWithScalar };