silgi 0.0.14 → 0.1.0-beta.2
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.
- package/README.md +102 -1
- package/dist/_virtual/_rolldown/runtime.mjs +5 -0
- package/dist/adapters/astro.d.mts +17 -0
- package/dist/adapters/astro.mjs +24 -0
- package/dist/adapters/aws-lambda.d.mts +31 -0
- package/dist/adapters/aws-lambda.mjs +85 -0
- package/dist/adapters/elysia.d.mts +17 -0
- package/dist/adapters/elysia.mjs +76 -0
- package/dist/adapters/express.d.mts +16 -0
- package/dist/adapters/express.mjs +78 -0
- package/dist/adapters/fastify.d.mts +15 -0
- package/dist/adapters/fastify.mjs +78 -0
- package/dist/adapters/message-port.d.mts +37 -0
- package/dist/adapters/message-port.mjs +129 -0
- package/dist/adapters/nestjs.d.mts +25 -0
- package/dist/adapters/nestjs.mjs +91 -0
- package/dist/adapters/nextjs.d.mts +21 -0
- package/dist/adapters/nextjs.mjs +30 -0
- package/dist/adapters/peer.d.mts +27 -0
- package/dist/adapters/peer.mjs +36 -0
- package/dist/adapters/remix.d.mts +17 -0
- package/dist/adapters/remix.mjs +24 -0
- package/dist/adapters/solidstart.d.mts +14 -0
- package/dist/adapters/solidstart.mjs +30 -0
- package/dist/adapters/sveltekit.d.mts +18 -0
- package/dist/adapters/sveltekit.mjs +33 -0
- package/dist/analyze.mjs +26 -0
- package/dist/broker/index.d.mts +62 -0
- package/dist/broker/index.mjs +153 -0
- package/dist/broker/nats.d.mts +33 -0
- package/dist/broker/nats.mjs +31 -0
- package/dist/broker/redis.d.mts +51 -0
- package/dist/broker/redis.mjs +92 -0
- package/dist/builder.d.mts +36 -0
- package/dist/builder.mjs +51 -0
- package/dist/callable.d.mts +17 -0
- package/dist/callable.mjs +42 -0
- package/dist/client/adapters/fetch/index.d.mts +17 -0
- package/dist/client/adapters/fetch/index.mjs +61 -0
- package/dist/client/adapters/ofetch/index.d.mts +41 -0
- package/dist/client/adapters/ofetch/index.mjs +92 -0
- package/dist/client/client.d.mts +29 -0
- package/dist/client/client.mjs +54 -0
- package/dist/client/dynamic-link.d.mts +15 -0
- package/dist/client/dynamic-link.mjs +16 -0
- package/dist/client/index.d.mts +7 -0
- package/dist/client/index.mjs +6 -0
- package/dist/client/interceptor.d.mts +31 -0
- package/dist/client/interceptor.mjs +34 -0
- package/dist/client/merge.d.mts +28 -0
- package/dist/client/merge.mjs +30 -0
- package/dist/client/openapi.d.mts +29 -0
- package/dist/client/openapi.mjs +89 -0
- package/dist/client/plugins/batch.d.mts +20 -0
- package/dist/client/plugins/batch.mjs +64 -0
- package/dist/client/plugins/csrf.d.mts +13 -0
- package/dist/client/plugins/csrf.mjs +20 -0
- package/dist/client/plugins/dedupe.d.mts +10 -0
- package/dist/client/plugins/dedupe.mjs +28 -0
- package/dist/client/plugins/index.d.mts +5 -0
- package/dist/client/plugins/index.mjs +5 -0
- package/dist/client/plugins/retry.d.mts +11 -0
- package/dist/client/plugins/retry.mjs +21 -0
- package/dist/client/server.d.mts +16 -0
- package/dist/client/server.mjs +60 -0
- package/dist/client/types.d.mts +29 -0
- package/dist/codec/devalue.d.mts +21 -0
- package/dist/codec/devalue.mjs +32 -0
- package/dist/codec/msgpack.d.mts +21 -0
- package/dist/codec/msgpack.mjs +59 -0
- package/dist/compile.d.mts +54 -0
- package/dist/compile.mjs +305 -0
- package/dist/contract.d.mts +36 -0
- package/dist/contract.mjs +40 -0
- package/dist/core/error.d.mts +104 -0
- package/dist/core/error.mjs +139 -0
- package/dist/core/handler.mjs +546 -0
- package/dist/core/iterator.d.mts +17 -0
- package/dist/core/iterator.mjs +79 -0
- package/dist/core/router-utils.mjs +16 -0
- package/dist/core/schema.d.mts +19 -0
- package/dist/core/schema.mjs +26 -0
- package/dist/core/serve.mjs +38 -0
- package/dist/core/sse.d.mts +16 -0
- package/dist/core/sse.mjs +95 -0
- package/dist/core/storage.d.mts +21 -0
- package/dist/core/storage.mjs +63 -0
- package/dist/core/utils.mjs +21 -0
- package/dist/fast-stringify.mjs +125 -0
- package/dist/index.d.mts +15 -37
- package/dist/index.mjs +13 -7
- package/dist/integrations/ai/index.d.mts +25 -0
- package/dist/integrations/ai/index.mjs +116 -0
- package/dist/integrations/react/index.d.mts +83 -0
- package/dist/integrations/react/index.mjs +197 -0
- package/dist/integrations/tanstack-query/index.d.mts +120 -0
- package/dist/integrations/tanstack-query/index.mjs +100 -0
- package/dist/integrations/tanstack-query/ssr.d.mts +51 -0
- package/dist/integrations/tanstack-query/ssr.mjs +89 -0
- package/dist/integrations/zod/converter.d.mts +75 -0
- package/dist/integrations/zod/converter.mjs +345 -0
- package/dist/integrations/zod/index.d.mts +2 -0
- package/dist/integrations/zod/index.mjs +2 -0
- package/dist/lazy.d.mts +24 -0
- package/dist/lazy.mjs +27 -0
- package/dist/lifecycle.d.mts +36 -0
- package/dist/lifecycle.mjs +46 -0
- package/dist/map-input.d.mts +17 -0
- package/dist/map-input.mjs +24 -0
- package/dist/plugins/analytics.d.mts +168 -0
- package/dist/plugins/analytics.mjs +459 -0
- package/dist/plugins/batch-server.d.mts +20 -0
- package/dist/plugins/batch-server.mjs +86 -0
- package/dist/plugins/body-limit.d.mts +16 -0
- package/dist/plugins/body-limit.mjs +44 -0
- package/dist/plugins/cache.d.mts +170 -0
- package/dist/plugins/cache.mjs +200 -0
- package/dist/plugins/coerce.d.mts +21 -0
- package/dist/plugins/coerce.mjs +46 -0
- package/dist/plugins/compression.d.mts +19 -0
- package/dist/plugins/compression.mjs +23 -0
- package/dist/plugins/cookies.d.mts +44 -0
- package/dist/plugins/cookies.mjs +67 -0
- package/dist/plugins/cors.d.mts +39 -0
- package/dist/plugins/cors.mjs +56 -0
- package/dist/plugins/custom-serializer.d.mts +57 -0
- package/dist/plugins/custom-serializer.mjs +40 -0
- package/dist/plugins/file-upload.d.mts +38 -0
- package/dist/plugins/file-upload.mjs +100 -0
- package/dist/plugins/index.d.mts +16 -0
- package/dist/plugins/index.mjs +16 -0
- package/dist/plugins/otel.d.mts +35 -0
- package/dist/plugins/otel.mjs +40 -0
- package/dist/plugins/pino.d.mts +60 -0
- package/dist/plugins/pino.mjs +42 -0
- package/dist/plugins/pubsub.d.mts +50 -0
- package/dist/plugins/pubsub.mjs +53 -0
- package/dist/plugins/ratelimit.d.mts +51 -0
- package/dist/plugins/ratelimit.mjs +81 -0
- package/dist/plugins/signing.d.mts +41 -0
- package/dist/plugins/signing.mjs +115 -0
- package/dist/plugins/strict-get.d.mts +10 -0
- package/dist/plugins/strict-get.mjs +33 -0
- package/dist/route/add.mjs +240 -0
- package/dist/route/compiler.mjs +373 -0
- package/dist/route/context.mjs +12 -0
- package/dist/route/types.d.mts +11 -0
- package/dist/route/utils.mjs +17 -0
- package/dist/scalar.d.mts +53 -0
- package/dist/scalar.mjs +330 -0
- package/dist/silgi.d.mts +139 -0
- package/dist/silgi.mjs +113 -0
- package/dist/trpc-interop.d.mts +22 -0
- package/dist/trpc-interop.mjs +68 -0
- package/dist/types.d.mts +82 -0
- package/dist/ws.d.mts +42 -0
- package/dist/ws.mjs +137 -0
- package/lib/dashboard/index.html +123 -0
- package/lib/ocache.d.mts +1 -0
- package/lib/ocache.mjs +1 -0
- package/lib/ofetch.d.mts +1 -0
- package/lib/ofetch.mjs +1 -0
- package/lib/srvx.d.mts +1 -0
- package/lib/srvx.mjs +1 -0
- package/lib/unstorage.d.mts +1 -0
- package/lib/unstorage.mjs +1 -0
- package/package.json +291 -65
- package/bin/silgi.mjs +0 -3
- package/dist/chunks/generate.mjs +0 -933
- package/dist/chunks/init.mjs +0 -21
- package/dist/cli/config.d.mts +0 -19
- package/dist/cli/config.d.ts +0 -19
- package/dist/cli/config.mjs +0 -5
- package/dist/cli/index.d.mts +0 -2
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.mjs +0 -119
- package/dist/index.d.ts +0 -37
- package/dist/plugins/openapi.d.mts +0 -138
- package/dist/plugins/openapi.d.ts +0 -138
- package/dist/plugins/openapi.mjs +0 -204
- package/dist/plugins/scalar.d.mts +0 -14
- package/dist/plugins/scalar.d.ts +0 -14
- package/dist/plugins/scalar.mjs +0 -66
- package/dist/shared/silgi.BMCYk2cR.mjs +0 -841
- package/dist/shared/silgi.D5qK9QOm.d.mts +0 -301
- 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 };
|