silgi 0.0.13 → 0.1.0-beta.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 (186) hide show
  1. package/README.md +102 -1
  2. package/dist/_virtual/_rolldown/runtime.mjs +5 -0
  3. package/dist/adapters/astro.d.mts +17 -0
  4. package/dist/adapters/astro.mjs +24 -0
  5. package/dist/adapters/aws-lambda.d.mts +31 -0
  6. package/dist/adapters/aws-lambda.mjs +85 -0
  7. package/dist/adapters/elysia.d.mts +17 -0
  8. package/dist/adapters/elysia.mjs +76 -0
  9. package/dist/adapters/express.d.mts +16 -0
  10. package/dist/adapters/express.mjs +78 -0
  11. package/dist/adapters/fastify.d.mts +15 -0
  12. package/dist/adapters/fastify.mjs +78 -0
  13. package/dist/adapters/message-port.d.mts +37 -0
  14. package/dist/adapters/message-port.mjs +129 -0
  15. package/dist/adapters/nestjs.d.mts +25 -0
  16. package/dist/adapters/nestjs.mjs +91 -0
  17. package/dist/adapters/nextjs.d.mts +21 -0
  18. package/dist/adapters/nextjs.mjs +30 -0
  19. package/dist/adapters/peer.d.mts +27 -0
  20. package/dist/adapters/peer.mjs +36 -0
  21. package/dist/adapters/remix.d.mts +17 -0
  22. package/dist/adapters/remix.mjs +24 -0
  23. package/dist/adapters/solidstart.d.mts +14 -0
  24. package/dist/adapters/solidstart.mjs +30 -0
  25. package/dist/adapters/sveltekit.d.mts +18 -0
  26. package/dist/adapters/sveltekit.mjs +33 -0
  27. package/dist/analyze.mjs +26 -0
  28. package/dist/broker/index.d.mts +62 -0
  29. package/dist/broker/index.mjs +153 -0
  30. package/dist/broker/nats.d.mts +33 -0
  31. package/dist/broker/nats.mjs +31 -0
  32. package/dist/broker/redis.d.mts +51 -0
  33. package/dist/broker/redis.mjs +92 -0
  34. package/dist/builder.d.mts +36 -0
  35. package/dist/builder.mjs +51 -0
  36. package/dist/callable.d.mts +17 -0
  37. package/dist/callable.mjs +42 -0
  38. package/dist/client/adapters/fetch/index.d.mts +17 -0
  39. package/dist/client/adapters/fetch/index.mjs +61 -0
  40. package/dist/client/adapters/ofetch/index.d.mts +41 -0
  41. package/dist/client/adapters/ofetch/index.mjs +92 -0
  42. package/dist/client/client.d.mts +29 -0
  43. package/dist/client/client.mjs +54 -0
  44. package/dist/client/dynamic-link.d.mts +15 -0
  45. package/dist/client/dynamic-link.mjs +16 -0
  46. package/dist/client/index.d.mts +7 -0
  47. package/dist/client/index.mjs +6 -0
  48. package/dist/client/interceptor.d.mts +31 -0
  49. package/dist/client/interceptor.mjs +34 -0
  50. package/dist/client/merge.d.mts +28 -0
  51. package/dist/client/merge.mjs +30 -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 +20 -0
  55. package/dist/client/plugins/batch.mjs +64 -0
  56. package/dist/client/plugins/csrf.d.mts +13 -0
  57. package/dist/client/plugins/csrf.mjs +20 -0
  58. package/dist/client/plugins/dedupe.d.mts +10 -0
  59. package/dist/client/plugins/dedupe.mjs +28 -0
  60. package/dist/client/plugins/index.d.mts +5 -0
  61. package/dist/client/plugins/index.mjs +5 -0
  62. package/dist/client/plugins/retry.d.mts +11 -0
  63. package/dist/client/plugins/retry.mjs +21 -0
  64. package/dist/client/server.d.mts +16 -0
  65. package/dist/client/server.mjs +60 -0
  66. package/dist/client/types.d.mts +29 -0
  67. package/dist/codec/devalue.d.mts +21 -0
  68. package/dist/codec/devalue.mjs +32 -0
  69. package/dist/codec/msgpack.d.mts +21 -0
  70. package/dist/codec/msgpack.mjs +59 -0
  71. package/dist/compile.d.mts +52 -0
  72. package/dist/compile.mjs +304 -0
  73. package/dist/contract.d.mts +36 -0
  74. package/dist/contract.mjs +40 -0
  75. package/dist/core/error.d.mts +104 -0
  76. package/dist/core/error.mjs +139 -0
  77. package/dist/core/handler.mjs +546 -0
  78. package/dist/core/iterator.d.mts +17 -0
  79. package/dist/core/iterator.mjs +79 -0
  80. package/dist/core/router-utils.mjs +16 -0
  81. package/dist/core/schema.d.mts +19 -0
  82. package/dist/core/schema.mjs +26 -0
  83. package/dist/core/serve.mjs +38 -0
  84. package/dist/core/sse.d.mts +16 -0
  85. package/dist/core/sse.mjs +95 -0
  86. package/dist/core/storage.d.mts +21 -0
  87. package/dist/core/storage.mjs +63 -0
  88. package/dist/core/utils.mjs +21 -0
  89. package/dist/fast-stringify.mjs +125 -0
  90. package/dist/index.d.mts +15 -37
  91. package/dist/index.mjs +13 -7
  92. package/dist/integrations/ai/index.d.mts +25 -0
  93. package/dist/integrations/ai/index.mjs +116 -0
  94. package/dist/integrations/react/index.d.mts +83 -0
  95. package/dist/integrations/react/index.mjs +197 -0
  96. package/dist/integrations/tanstack-query/index.d.mts +120 -0
  97. package/dist/integrations/tanstack-query/index.mjs +100 -0
  98. package/dist/integrations/tanstack-query/ssr.d.mts +51 -0
  99. package/dist/integrations/tanstack-query/ssr.mjs +89 -0
  100. package/dist/integrations/zod/converter.d.mts +75 -0
  101. package/dist/integrations/zod/converter.mjs +345 -0
  102. package/dist/integrations/zod/index.d.mts +2 -0
  103. package/dist/integrations/zod/index.mjs +2 -0
  104. package/dist/lazy.d.mts +24 -0
  105. package/dist/lazy.mjs +27 -0
  106. package/dist/lifecycle.d.mts +36 -0
  107. package/dist/lifecycle.mjs +46 -0
  108. package/dist/map-input.d.mts +17 -0
  109. package/dist/map-input.mjs +24 -0
  110. package/dist/plugins/analytics.d.mts +168 -0
  111. package/dist/plugins/analytics.mjs +459 -0
  112. package/dist/plugins/batch-server.d.mts +20 -0
  113. package/dist/plugins/batch-server.mjs +86 -0
  114. package/dist/plugins/body-limit.d.mts +16 -0
  115. package/dist/plugins/body-limit.mjs +44 -0
  116. package/dist/plugins/cache.d.mts +170 -0
  117. package/dist/plugins/cache.mjs +200 -0
  118. package/dist/plugins/coerce.d.mts +21 -0
  119. package/dist/plugins/coerce.mjs +46 -0
  120. package/dist/plugins/compression.d.mts +19 -0
  121. package/dist/plugins/compression.mjs +23 -0
  122. package/dist/plugins/cookies.d.mts +44 -0
  123. package/dist/plugins/cookies.mjs +67 -0
  124. package/dist/plugins/cors.d.mts +39 -0
  125. package/dist/plugins/cors.mjs +56 -0
  126. package/dist/plugins/custom-serializer.d.mts +57 -0
  127. package/dist/plugins/custom-serializer.mjs +40 -0
  128. package/dist/plugins/file-upload.d.mts +38 -0
  129. package/dist/plugins/file-upload.mjs +100 -0
  130. package/dist/plugins/index.d.mts +16 -0
  131. package/dist/plugins/index.mjs +16 -0
  132. package/dist/plugins/otel.d.mts +35 -0
  133. package/dist/plugins/otel.mjs +40 -0
  134. package/dist/plugins/pino.d.mts +60 -0
  135. package/dist/plugins/pino.mjs +42 -0
  136. package/dist/plugins/pubsub.d.mts +50 -0
  137. package/dist/plugins/pubsub.mjs +53 -0
  138. package/dist/plugins/ratelimit.d.mts +51 -0
  139. package/dist/plugins/ratelimit.mjs +81 -0
  140. package/dist/plugins/signing.d.mts +41 -0
  141. package/dist/plugins/signing.mjs +115 -0
  142. package/dist/plugins/strict-get.d.mts +10 -0
  143. package/dist/plugins/strict-get.mjs +33 -0
  144. package/dist/route/add.mjs +240 -0
  145. package/dist/route/compiler.mjs +373 -0
  146. package/dist/route/context.mjs +12 -0
  147. package/dist/route/types.d.mts +11 -0
  148. package/dist/route/utils.mjs +17 -0
  149. package/dist/scalar.d.mts +53 -0
  150. package/dist/scalar.mjs +315 -0
  151. package/dist/silgi.d.mts +139 -0
  152. package/dist/silgi.mjs +113 -0
  153. package/dist/trpc-interop.d.mts +22 -0
  154. package/dist/trpc-interop.mjs +68 -0
  155. package/dist/types.d.mts +82 -0
  156. package/dist/ws.d.mts +42 -0
  157. package/dist/ws.mjs +137 -0
  158. package/lib/dashboard/index.html +123 -0
  159. package/lib/ocache.d.mts +1 -0
  160. package/lib/ocache.mjs +1 -0
  161. package/lib/ofetch.d.mts +1 -0
  162. package/lib/ofetch.mjs +1 -0
  163. package/lib/srvx.d.mts +1 -0
  164. package/lib/srvx.mjs +1 -0
  165. package/lib/unstorage.d.mts +1 -0
  166. package/lib/unstorage.mjs +1 -0
  167. package/package.json +291 -65
  168. package/bin/silgi.mjs +0 -3
  169. package/dist/chunks/generate.mjs +0 -933
  170. package/dist/chunks/init.mjs +0 -21
  171. package/dist/cli/config.d.mts +0 -19
  172. package/dist/cli/config.d.ts +0 -19
  173. package/dist/cli/config.mjs +0 -5
  174. package/dist/cli/index.d.mts +0 -2
  175. package/dist/cli/index.d.ts +0 -2
  176. package/dist/cli/index.mjs +0 -119
  177. package/dist/index.d.ts +0 -37
  178. package/dist/plugins/openapi.d.mts +0 -138
  179. package/dist/plugins/openapi.d.ts +0 -138
  180. package/dist/plugins/openapi.mjs +0 -204
  181. package/dist/plugins/scalar.d.mts +0 -14
  182. package/dist/plugins/scalar.d.ts +0 -14
  183. package/dist/plugins/scalar.mjs +0 -66
  184. package/dist/shared/silgi.BMCYk2cR.mjs +0 -841
  185. package/dist/shared/silgi.D5qK9QOm.d.mts +0 -301
  186. package/dist/shared/silgi.D5qK9QOm.d.ts +0 -301
@@ -0,0 +1,53 @@
1
+ //#region src/plugins/pubsub.ts
2
+ var MemoryPubSub = class {
3
+ #listeners = /* @__PURE__ */ new Map();
4
+ async publish(channel, data) {
5
+ const listeners = this.#listeners.get(channel);
6
+ if (!listeners) return;
7
+ for (const cb of listeners) cb(data);
8
+ }
9
+ subscribe(channel, callback) {
10
+ let set = this.#listeners.get(channel);
11
+ if (!set) {
12
+ set = /* @__PURE__ */ new Set();
13
+ this.#listeners.set(channel, set);
14
+ }
15
+ set.add(callback);
16
+ return () => {
17
+ set.delete(callback);
18
+ if (set.size === 0) this.#listeners.delete(channel);
19
+ };
20
+ }
21
+ };
22
+ /**
23
+ * Create a publisher from a PubSub backend.
24
+ *
25
+ * The publisher exposes `publish()` for mutations and `subscribe()`
26
+ * as an async generator for SSE/WebSocket subscriptions.
27
+ */
28
+ function createPublisher(backend) {
29
+ return {
30
+ publish: (channel, data) => backend.publish(channel, data),
31
+ async *subscribe(channel) {
32
+ const queue = [];
33
+ let resolve = null;
34
+ const unsubscribe = backend.subscribe(channel, (data) => {
35
+ queue.push(data);
36
+ if (resolve) {
37
+ resolve();
38
+ resolve = null;
39
+ }
40
+ });
41
+ try {
42
+ while (true) if (queue.length > 0) yield queue.shift();
43
+ else await new Promise((r) => {
44
+ resolve = r;
45
+ });
46
+ } finally {
47
+ unsubscribe();
48
+ }
49
+ }
50
+ };
51
+ }
52
+ //#endregion
53
+ export { MemoryPubSub, createPublisher };
@@ -0,0 +1,51 @@
1
+ import { GuardDef } from "../types.mjs";
2
+
3
+ //#region src/plugins/ratelimit.d.ts
4
+ interface RateLimitResult {
5
+ success: boolean;
6
+ limit: number;
7
+ remaining: number;
8
+ reset: number;
9
+ }
10
+ interface RateLimiter {
11
+ limit(key: string): Promise<RateLimitResult>;
12
+ }
13
+ interface MemoryRateLimiterOptions {
14
+ /** Maximum requests per window */
15
+ limit: number;
16
+ /** Window duration in milliseconds */
17
+ windowMs: number;
18
+ }
19
+ declare class MemoryRateLimiter implements RateLimiter {
20
+ #private;
21
+ constructor(options: MemoryRateLimiterOptions);
22
+ limit(key: string): Promise<RateLimitResult>;
23
+ }
24
+ interface RateLimitGuardOptions {
25
+ /** The rate limiter instance */
26
+ limiter: RateLimiter;
27
+ /** Extract rate limit key from context */
28
+ keyFn: (ctx: Record<string, unknown>) => string | Promise<string>;
29
+ /** Custom error message */
30
+ message?: string;
31
+ }
32
+ /**
33
+ * Create a rate limiting guard.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { rateLimitGuard, MemoryRateLimiter } from "silgi/ratelimit"
38
+ *
39
+ * const rateLimit = rateLimitGuard({
40
+ * limiter: new MemoryRateLimiter({ limit: 100, windowMs: 60_000 }),
41
+ * keyFn: (ctx) => (ctx as any).ip ?? "anonymous",
42
+ * })
43
+ *
44
+ * const proc = k
45
+ * .$use(rateLimit)
46
+ * .$resolve(() => ({ ok: true }))
47
+ * ```
48
+ */
49
+ declare function rateLimitGuard(options: RateLimitGuardOptions): GuardDef<any, any>;
50
+ //#endregion
51
+ export { MemoryRateLimiter, MemoryRateLimiterOptions, RateLimitGuardOptions, RateLimitResult, RateLimiter, rateLimitGuard };
@@ -0,0 +1,81 @@
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
+ constructor(options) {
14
+ this.#limit = options.limit;
15
+ this.#windowMs = options.windowMs;
16
+ }
17
+ async limit(key) {
18
+ const now = Date.now();
19
+ const windowStart = now - this.#windowMs;
20
+ let timestamps = this.#store.get(key);
21
+ if (!timestamps) {
22
+ timestamps = [];
23
+ this.#store.set(key, timestamps);
24
+ }
25
+ while (timestamps.length > 0 && timestamps[0] < windowStart) timestamps.shift();
26
+ const remaining = Math.max(0, this.#limit - timestamps.length);
27
+ const reset = timestamps.length > 0 ? timestamps[0] + this.#windowMs : now + this.#windowMs;
28
+ if (timestamps.length >= this.#limit) return {
29
+ success: false,
30
+ limit: this.#limit,
31
+ remaining: 0,
32
+ reset
33
+ };
34
+ timestamps.push(now);
35
+ return {
36
+ success: true,
37
+ limit: this.#limit,
38
+ remaining: remaining - 1,
39
+ reset
40
+ };
41
+ }
42
+ };
43
+ /**
44
+ * Create a rate limiting guard.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { rateLimitGuard, MemoryRateLimiter } from "silgi/ratelimit"
49
+ *
50
+ * const rateLimit = rateLimitGuard({
51
+ * limiter: new MemoryRateLimiter({ limit: 100, windowMs: 60_000 }),
52
+ * keyFn: (ctx) => (ctx as any).ip ?? "anonymous",
53
+ * })
54
+ *
55
+ * const proc = k
56
+ * .$use(rateLimit)
57
+ * .$resolve(() => ({ ok: true }))
58
+ * ```
59
+ */
60
+ function rateLimitGuard(options) {
61
+ return {
62
+ kind: "guard",
63
+ fn: async (ctx) => {
64
+ const key = await options.keyFn(ctx);
65
+ const result = await options.limiter.limit(key);
66
+ if (!result.success) throw new SilgiError("TOO_MANY_REQUESTS", {
67
+ status: 429,
68
+ message: options.message ?? "Rate limit exceeded",
69
+ data: {
70
+ limit: result.limit,
71
+ remaining: result.remaining,
72
+ reset: result.reset,
73
+ retryAfter: Math.ceil((result.reset - Date.now()) / 1e3)
74
+ }
75
+ });
76
+ return { rateLimit: result };
77
+ }
78
+ };
79
+ }
80
+ //#endregion
81
+ 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,115 @@
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
+ return btoa(String.fromCharCode(...data)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
107
+ }
108
+ function base64urlDecode(str) {
109
+ const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
110
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
111
+ const binary = atob(padded);
112
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
113
+ }
114
+ //#endregion
115
+ 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,240 @@
1
+ import { isCatchAll, splitPath } from "./utils.mjs";
2
+ //#region src/route/add.ts
3
+ /**
4
+ * Add a route to the router.
5
+ *
6
+ * Supports all rou3 patterns:
7
+ * - Static: `/users/list`
8
+ * - Params: `/users/:id`
9
+ * - Regex params: `/users/:id(\\d+)`
10
+ * - Unnamed regex: `/path/(\\d+)`
11
+ * - Wildcards: `/files/**`, `/files/**:rest`
12
+ * - Single wildcard: `/blog/*`
13
+ * - Wildcard patterns: `/files/*.png`, `/files/file-*-*.png`
14
+ * - Optional: `/users/:id?`, `/api/:version?/users`
15
+ * - One-or-more: `/files/:path+`
16
+ * - Zero-or-more: `/files/:path*`
17
+ * - Non-capturing groups: `/book{s}?`, `/blog/:id(\\d+){-:title}?`, `/foo{/bar}?`
18
+ * - Mixed params: `/npm/@:param1/:param2`
19
+ * - Escaped: `/static\\:path/\\*`
20
+ */
21
+ function addRoute(ctx, method, path, data) {
22
+ const hasEscapes = /\\[:*(){}]/.test(path);
23
+ if (hasEscapes) path = path.replace(/\\:/g, "�A").replace(/\\\*/g, "�B").replace(/\\\(/g, "�C").replace(/\\\)/g, "�D").replace(/\\\{/g, "�E").replace(/\\\}/g, "�F");
24
+ const expanded = expandGroups(path);
25
+ if (expanded) {
26
+ for (const p of expanded) addRoute(ctx, method, hasEscapes ? p : p, data);
27
+ return;
28
+ }
29
+ const modExpanded = expandModifiers(path);
30
+ if (modExpanded) {
31
+ for (const p of modExpanded) addRoute(ctx, method, p, data);
32
+ return;
33
+ }
34
+ const segments = splitPath(path);
35
+ const paramMap = [];
36
+ const paramRegex = [];
37
+ let hasRegex = false;
38
+ let isStatic = true;
39
+ let node = ctx.root;
40
+ for (let i = 0; i < segments.length; i++) {
41
+ let segment = segments[i];
42
+ if (hasEscapes) {
43
+ segment = decodeEscapes(segment);
44
+ segments[i] = segment;
45
+ }
46
+ if (isCatchAll(segment)) {
47
+ isStatic = false;
48
+ if (!node.wildcard) node.wildcard = { key: "**" };
49
+ node = node.wildcard;
50
+ if (segment.length > 2 && segment.charCodeAt(2) === 58) paramMap.push([
51
+ i,
52
+ segment.slice(3),
53
+ false
54
+ ]);
55
+ else paramMap.push([
56
+ i,
57
+ "_",
58
+ false
59
+ ]);
60
+ break;
61
+ }
62
+ if (segment === "*" || segment.includes("*") && !segment.startsWith("**")) {
63
+ isStatic = false;
64
+ if (segment === "*") _setMethod(node, method, data, [...paramMap], [...paramRegex], hasRegex, false);
65
+ if (!node.param) node.param = { key: "*" };
66
+ node = node.param;
67
+ paramMap.push([
68
+ i,
69
+ String(paramMap.length),
70
+ true
71
+ ]);
72
+ if (segment !== "*") {
73
+ const escaped = segment.replace(/[.*+?^${}()|[\]\\]/g, (c) => c === "*" ? "([^/]+?)" : `\\${c}`);
74
+ paramRegex[i] = new RegExp(`^${escaped}$`);
75
+ hasRegex = true;
76
+ }
77
+ continue;
78
+ }
79
+ if (segment.startsWith("(") && segment.endsWith(")")) {
80
+ isStatic = false;
81
+ if (!node.param) node.param = { key: "*" };
82
+ node = node.param;
83
+ const pattern = segment.slice(1, -1);
84
+ paramMap.push([
85
+ i,
86
+ String(paramMap.length),
87
+ false
88
+ ]);
89
+ paramRegex[i] = new RegExp(`^${pattern}$`);
90
+ hasRegex = true;
91
+ node.hasRegex = true;
92
+ continue;
93
+ }
94
+ if (segment.includes(":") && !hasEscapes) {
95
+ isStatic = false;
96
+ if (segment.charCodeAt(0) !== 58 || segment.indexOf(":", 1) !== -1) {
97
+ if (!node.param) node.param = { key: "*" };
98
+ node = node.param;
99
+ const { regex, names } = parseMixedSegment(segment);
100
+ paramMap.push([
101
+ i,
102
+ regex,
103
+ false
104
+ ]);
105
+ paramRegex[i] = new RegExp(`^${regex.source}$`);
106
+ hasRegex = true;
107
+ node.hasRegex = true;
108
+ for (const name of names) paramMap.push([
109
+ i,
110
+ name,
111
+ false
112
+ ]);
113
+ paramMap.splice(paramMap.length - names.length - 1, 1);
114
+ continue;
115
+ }
116
+ let paramSeg = segment.slice(1);
117
+ if (!node.param) node.param = { key: "*" };
118
+ let optional = false;
119
+ if (paramSeg.endsWith("?")) {
120
+ optional = true;
121
+ paramSeg = paramSeg.slice(0, -1);
122
+ _setMethod(node, method, data, [...paramMap], [...paramRegex], hasRegex, false);
123
+ }
124
+ node = node.param;
125
+ const parenIdx = paramSeg.indexOf("(");
126
+ if (parenIdx !== -1) {
127
+ const name = paramSeg.slice(0, parenIdx);
128
+ const pattern = paramSeg.slice(parenIdx + 1, -1);
129
+ paramMap.push([
130
+ i,
131
+ name,
132
+ optional
133
+ ]);
134
+ paramRegex[i] = new RegExp(`^${pattern}$`);
135
+ hasRegex = true;
136
+ node.hasRegex = true;
137
+ } else paramMap.push([
138
+ i,
139
+ paramSeg,
140
+ optional
141
+ ]);
142
+ continue;
143
+ }
144
+ if (!node.static) node.static = Object.create(null);
145
+ if (!node.static[segment]) node.static[segment] = { key: segment };
146
+ node = node.static[segment];
147
+ }
148
+ _setMethod(node, method, data, paramMap, paramRegex, hasRegex, !isStatic && _lastIsCatchAll(segments));
149
+ if (isStatic) {
150
+ const normalized = "/" + segments.join("/");
151
+ ctx.static[normalized] = node;
152
+ if (normalized.length > 1) ctx.static[normalized + "/"] = node;
153
+ }
154
+ }
155
+ function expandGroups(path) {
156
+ const match = path.match(/\{([^}]+)\}\?/);
157
+ if (!match) return null;
158
+ const before = path.slice(0, match.index);
159
+ const content = match[1];
160
+ const after = path.slice(match.index + match[0].length);
161
+ return [before + content + after, before + after];
162
+ }
163
+ function expandModifiers(path) {
164
+ const segments = path.split("/");
165
+ for (let i = 0; i < segments.length; i++) {
166
+ const m = segments[i].match(/^(.*:[\w-]+(?:\([^)]*\))?)([?+*])$/);
167
+ if (!m) continue;
168
+ const pre = segments.slice(0, i);
169
+ const suf = segments.slice(i + 1);
170
+ const modifier = m[2];
171
+ const baseName = m[1].match(/:([\w-]+)/)?.[1] || "_";
172
+ const cleanPre = pre.filter(Boolean);
173
+ if (modifier === "?") {
174
+ if (i < segments.length - 1) return ["/" + cleanPre.concat(m[1]).concat(suf).join("/"), "/" + cleanPre.concat(suf).join("/")];
175
+ } else if (modifier === "+") return ["/" + [
176
+ ...cleanPre,
177
+ `**:${baseName}`,
178
+ ...suf
179
+ ].join("/")];
180
+ else if (modifier === "*") return ["/" + [
181
+ ...cleanPre,
182
+ `**:${baseName}`,
183
+ ...suf
184
+ ].join("/"), "/" + [...cleanPre, ...suf].join("/")];
185
+ }
186
+ return null;
187
+ }
188
+ function parseMixedSegment(segment) {
189
+ const names = [];
190
+ let pattern = "";
191
+ let i = 0;
192
+ while (i < segment.length) if (segment[i] === ":") {
193
+ let j = i + 1;
194
+ while (j < segment.length && /[\w-]/.test(segment[j])) j++;
195
+ if (j < segment.length && segment[j] === "(") {
196
+ const end = segment.indexOf(")", j);
197
+ const name = segment.slice(i + 1, j);
198
+ const constraint = segment.slice(j + 1, end);
199
+ names.push(name);
200
+ pattern += `(?<${name.replace(/-/g, "_")}>${constraint})`;
201
+ i = end + 1;
202
+ } else {
203
+ const name = segment.slice(i + 1, j);
204
+ names.push(name);
205
+ pattern += `(?<${name.replace(/-/g, "_")}>[^/]+?)`;
206
+ i = j;
207
+ }
208
+ } else {
209
+ pattern += segment[i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
210
+ i++;
211
+ }
212
+ return {
213
+ regex: new RegExp(pattern),
214
+ names
215
+ };
216
+ }
217
+ function decodeEscapes(segment) {
218
+ return segment.replace(/\uFFFDA/g, ":").replace(/\uFFFDB/g, "*").replace(/\uFFFDC/g, "(").replace(/\uFFFDD/g, ")").replace(/\uFFFDE/g, "{").replace(/\uFFFDF/g, "}");
219
+ }
220
+ function _setMethod(node, method, data, paramMap, paramRegex, hasRegex, catchAll) {
221
+ if (!node.methods) node.methods = Object.create(null);
222
+ const entry = {
223
+ data,
224
+ paramMap: paramMap.length > 0 ? paramMap : void 0,
225
+ paramRegex,
226
+ catchAll: catchAll || void 0
227
+ };
228
+ if (hasRegex) node.hasRegex = true;
229
+ const key = method || "";
230
+ if (!node.methods[key]) node.methods[key] = [];
231
+ if (hasRegex) node.methods[key].unshift(entry);
232
+ else node.methods[key].push(entry);
233
+ }
234
+ function _lastIsCatchAll(segments) {
235
+ if (segments.length === 0) return false;
236
+ const last = segments[segments.length - 1];
237
+ return last === "**" || last.startsWith("**:");
238
+ }
239
+ //#endregion
240
+ export { addRoute };