akanjs 3.0.0 → 3.0.2-rc.0
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/CHANGELOG.md +3670 -0
- package/base/base.ts +1 -1
- package/client/cookie.ts +4 -6
- package/client/csrTypes.ts +2 -2
- package/common/Logger.ts +10 -0
- package/common/index.ts +0 -1
- package/common/routeConvention.ts +1 -9
- package/dictionary/base.dictionary.ts +7 -0
- package/dictionary/trans.ts +7 -1
- package/document/documentQuery.ts +6 -1
- package/document/index.ts +1 -0
- package/document/into.ts +15 -7
- package/document/liveRowScope.ts +79 -0
- package/fetch/client/fetchClient.ts +64 -12
- package/fetch/client/httpClient.ts +13 -4
- package/index.ts +22 -0
- package/package.json +1 -1
- package/server/akanOption.ts +20 -1
- package/server/akanServer.ts +5 -0
- package/server/cachePolicy.ts +12 -2
- package/server/crawler.ts +6 -0
- package/server/head.tsx +0 -4
- package/server/mcp/McpAuth.ts +3 -2
- package/server/mcp/McpExecutionContext.ts +15 -14
- package/server/mcp/McpRateLimiter.ts +18 -67
- package/server/proxy/webProxyRunner.ts +2 -0
- package/server/resolver/database.resolver.ts +28 -13
- package/server/resolver/signal.resolver.ts +75 -35
- package/server/robots.ts +1 -23
- package/server/routeTreeBuilder.ts +4 -2
- package/server/rscWorker.tsx +12 -24
- package/server/rscWorkerHost.ts +16 -7
- package/server/ssrFromRscRenderer.tsx +19 -15
- package/server/ssrTypes.ts +9 -0
- package/server/webRouter.ts +9 -2
- package/service/index.ts +1 -0
- package/service/predefinedAdaptor/index.ts +1 -0
- package/service/predefinedAdaptor/rateLimit.adaptor.ts +15 -0
- package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +119 -27
- package/service/predefinedAdaptor/sql/types.ts +14 -12
- package/service/rateLimiter.ts +80 -0
- package/signal/base.signal.ts +7 -6
- package/signal/endpointRateLimit.ts +159 -0
- package/signal/index.ts +1 -0
- package/signal/middleware.ts +6 -5
- package/signal/serializer/fetch.serializer.ts +15 -4
- package/signal/signalContext.ts +148 -67
- package/signal/slice.ts +30 -9
- package/signal/types.ts +8 -0
- package/test/signalTestRuntime.ts +2 -1
- package/types/base/base.d.ts +1 -1
- package/types/client/cookie.d.ts +1 -1
- package/types/client/csrTypes.d.ts +2 -2
- package/types/common/Logger.d.ts +2 -0
- package/types/common/index.d.ts +1 -1
- package/types/common/routeConvention.d.ts +0 -2
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/document/documentQuery.d.ts +5 -1
- package/types/document/index.d.ts +1 -0
- package/types/document/into.d.ts +7 -7
- package/types/document/liveRowScope.d.ts +9 -0
- package/types/fetch/client/fetchClient.d.ts +9 -1
- package/types/index.d.ts +32 -0
- package/types/server/akanOption.d.ts +7 -1
- package/types/server/cachePolicy.d.ts +2 -1
- package/types/server/crawler.d.ts +1 -0
- package/types/server/head.d.ts +0 -3
- package/types/server/mcp/McpExecutionContext.d.ts +4 -1
- package/types/server/mcp/McpRateLimiter.d.ts +2 -6
- package/types/server/rscWorkerHost.d.ts +3 -0
- package/types/server/ssrFromRscRenderer.d.ts +3 -1
- package/types/server/ssrTypes.d.ts +8 -0
- package/types/service/index.d.ts +1 -0
- package/types/service/predefinedAdaptor/index.d.ts +1 -0
- package/types/service/predefinedAdaptor/rateLimit.adaptor.d.ts +10 -0
- package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +6 -10
- package/types/service/predefinedAdaptor/sql/types.d.ts +13 -17
- package/types/service/rateLimiter.d.ts +24 -0
- package/types/signal/endpointRateLimit.d.ts +37 -0
- package/types/signal/index.d.ts +1 -0
- package/types/signal/signalContext.d.ts +33 -15
- package/types/signal/slice.d.ts +17 -5
- package/types/signal/types.d.ts +8 -0
- package/types/test/signalTestRuntime.d.ts +1 -1
- package/types/ui/recipe/badgeRecipe.d.ts +2 -2
- package/types/ui/recipe/buttonRecipe.d.ts +4 -4
- package/types/ui/recipe/inputRecipe.d.ts +3 -3
- package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +33 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/bplist.d.ts +11 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/dmg.d.ts +59 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/dsStore.d.ts +26 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/macAlias.d.ts +24 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/macossigning.d.ts +0 -1
- package/types/vendor/@akanjs/native/packages/cli/src/lib/project.d.ts +3 -1
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/linux-appimage.d.ts +6 -1
- package/types/vendor/@akanjs/native/packages/desktop/src/deeplinks.d.ts +9 -2
- package/types/webkit/RenderFailure.d.ts +6 -0
- package/types/webkit/useFrameRuntime.d.ts +0 -1
- package/ui/Input.tsx +7 -1
- package/ui/recipe/buttonRecipe.ts +1 -0
- package/ui/recipe/inputRecipe.ts +13 -7
- package/ui/styles.css +1 -0
- package/vendor/@akanjs/native/native/desktop/src/lib.rs +8 -1
- package/vendor/@akanjs/native/native/macos/dmg/background.html +18 -0
- package/vendor/@akanjs/native/native/macos/dmg/background.png +0 -0
- package/vendor/@akanjs/native/native/macos/dmg/background@2x.png +0 -0
- package/vendor/@akanjs/native/packages/cli/src/config.ts +23 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/addons.ts +2 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/bplist.ts +91 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +9 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/dmg.ts +331 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/dsStore.ts +191 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/exec.ts +11 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/macAlias.ts +101 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/macossigning.ts +1 -28
- package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +6 -1
- package/vendor/@akanjs/native/packages/cli/src/platforms/linux-appimage.ts +17 -7
- package/vendor/@akanjs/native/packages/cli/src/platforms/linux.ts +6 -1
- package/vendor/@akanjs/native/packages/cli/src/platforms/macos.ts +5 -2
- package/vendor/@akanjs/native/packages/desktop/src/deeplinks.ts +33 -10
- package/vendor/@akanjs/native/packages/desktop/src/host.ts +1 -1
- package/vendor/use-agentic/CHANGELOG.md +663 -0
- package/webkit/CsrRouteTable.ts +0 -2
- package/webkit/RenderFailure.tsx +32 -0
- package/webkit/RenderLayer.tsx +22 -2
- package/webkit/useCsrValues.ts +21 -11
package/server/cachePolicy.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface RouteCacheKeyInput {
|
|
|
7
7
|
request: Request;
|
|
8
8
|
url: URL;
|
|
9
9
|
theme?: string;
|
|
10
|
+
variant?: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export interface RouteCacheRenderState {
|
|
@@ -116,7 +117,7 @@ export function isRouteCachePathAllowed(
|
|
|
116
117
|
return matches(allow);
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
export function createRouteCacheKey({ request, url, theme = "" }: RouteCacheKeyInput): string {
|
|
120
|
+
export function createRouteCacheKey({ request, url, theme = "", variant }: RouteCacheKeyInput): string {
|
|
120
121
|
return [
|
|
121
122
|
getClientFacingOrigin(request, url),
|
|
122
123
|
request.headers.get("x-base-path") ?? "",
|
|
@@ -126,6 +127,7 @@ export function createRouteCacheKey({ request, url, theme = "" }: RouteCacheKeyI
|
|
|
126
127
|
url.search,
|
|
127
128
|
request.headers.get("accept-language") ?? "",
|
|
128
129
|
theme,
|
|
130
|
+
...(variant ? [variant] : []),
|
|
129
131
|
].join("\n");
|
|
130
132
|
}
|
|
131
133
|
|
|
@@ -153,7 +155,15 @@ export function resolvePublicRouteCacheEntryDecision(input: PublicRouteCacheEntr
|
|
|
153
155
|
return { entry: null, reason: "path-excluded" };
|
|
154
156
|
}
|
|
155
157
|
if (!isPublicRouteCacheableRequest(input.request)) return { entry: null, reason: "request-not-public" };
|
|
156
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
entry: createRouteCacheEntry({
|
|
160
|
+
request: input.request,
|
|
161
|
+
url: input.url,
|
|
162
|
+
theme: input.theme,
|
|
163
|
+
variant: input.variant,
|
|
164
|
+
ttl,
|
|
165
|
+
}),
|
|
166
|
+
};
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
export function resolveRouteCacheStoreTtl(baseTtl: number, state: RouteCacheRenderState): number | null {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
|
|
2
|
+
const crawlerPattern =
|
|
3
|
+
/(?<!cu)bot|crawl|spider|slurp|-user\b|preview|facebookexternalhit|facebookcatalog|meta-external|kakaotalk-scrap|whatsapp|vkshare|embedly|iframely|mastodon|anthropic-ai|cohere-ai|google-inspectiontool|googleother|mediapartners-google|yeti\b|daum\/|daumoa|ia_archiver/i;
|
|
4
|
+
|
|
5
|
+
export const isCrawlerUserAgent = (userAgent: string | null | undefined): boolean =>
|
|
6
|
+
!!userAgent && crawlerPattern.test(userAgent);
|
package/server/head.tsx
CHANGED
|
@@ -39,10 +39,6 @@ export function renderAkanHeadSnapshot(snapshot: AkanHeadSnapshotV1): Head {
|
|
|
39
39
|
);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export function shouldRenderLocaleAlternates(options: { isSpecialRoute?: boolean }): boolean {
|
|
43
|
-
return options.isSpecialRoute !== true;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
42
|
function isResolvedHead(value: unknown): value is ResolvedHead {
|
|
47
43
|
return isRecord(value) && "node" in value;
|
|
48
44
|
}
|
package/server/mcp/McpAuth.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { PromiseOrObject } from "akanjs/base";
|
|
3
|
-
import {
|
|
3
|
+
import { SignalContext } from "../../signal/signalContext";
|
|
4
4
|
import type { HttpRoutes } from "../types";
|
|
5
5
|
|
|
6
6
|
export interface McpAuthOption {
|
|
@@ -157,7 +157,8 @@ export class McpAuth {
|
|
|
157
157
|
|
|
158
158
|
static callerKey(req: Request): string {
|
|
159
159
|
const token = McpAuth.#bearer(req);
|
|
160
|
-
|
|
160
|
+
|
|
161
|
+
if (!token) return `ip:${SignalContext.clientIpOf(req) ?? "anonymous"}`;
|
|
161
162
|
const claims = McpAuth.#claims(token);
|
|
162
163
|
const id = [claims?.sid, claims?.jti, claims?.sub].find((value) => typeof value === "string" && value);
|
|
163
164
|
return typeof id === "string" ? `token:${id}` : `token:${createHash("sha256").update(token).digest("base64url")}`;
|
|
@@ -19,23 +19,24 @@ export class McpExecutionContext extends HttpExecutionContext {
|
|
|
19
19
|
this.#arguments = args;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
override async
|
|
22
|
+
override async readArgs(endpointInfo: EndpointInfo): Promise<unknown[]> {
|
|
23
23
|
const declared = new Set(endpointInfo.args.filter(McpExecutionContext.#describable).map((arg) => arg.name));
|
|
24
24
|
const undeclared = Object.keys(this.#arguments).find((name) => !declared.has(name));
|
|
25
25
|
if (undeclared) throw new McpArgumentError(`Unknown argument "${undeclared}".`);
|
|
26
|
-
return endpointInfo.args.map((arg) =>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
26
|
+
return endpointInfo.args.map((arg) => McpExecutionContext.#lift(arg, this.#arguments[arg.name] ?? null));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override parseArg(arg: McpArg, value: unknown): unknown {
|
|
30
|
+
try {
|
|
31
|
+
return deserialize(arg.argRef, arg.arrDepth, value, {
|
|
32
|
+
key: arg.name,
|
|
33
|
+
nullable: arg.option?.nullable,
|
|
34
|
+
enum: arg.enum,
|
|
35
|
+
});
|
|
36
|
+
} catch {
|
|
37
|
+
|
|
38
|
+
throw new McpArgumentError(McpExecutionContext.#argumentMessage(arg, value));
|
|
39
|
+
}
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
override makeResponse(result: unknown, endpointInfo: EndpointInfo) {
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { Logger } from "akanjs/common";
|
|
1
|
+
import { type RateCounter, RateLimiter } from "akanjs/service";
|
|
3
2
|
|
|
4
|
-
export
|
|
5
|
-
incr(topic: string, key: string, by?: number, option?: { expireAt?: Dayjs }): Promise<number>;
|
|
6
|
-
}
|
|
3
|
+
export type McpSharedCounter = RateCounter;
|
|
7
4
|
|
|
8
5
|
export interface McpRateLimitOption {
|
|
9
6
|
/** Per caller per window; `0` turns the counter off. */
|
|
@@ -17,23 +14,14 @@ export type McpRateLimitVerdict =
|
|
|
17
14
|
| { ok: true; release: () => void }
|
|
18
15
|
| { ok: false; reason: "calls" | "concurrent"; retryAfterMs: number };
|
|
19
16
|
|
|
20
|
-
interface McpRateBucket {
|
|
21
|
-
windowStart: number;
|
|
22
|
-
count: number;
|
|
23
|
-
inFlight: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
17
|
export class McpRateLimiter {
|
|
27
18
|
static readonly defaults = { calls: 120, windowMs: 60_000, concurrent: 8 } as const;
|
|
28
|
-
static readonly #logger = new Logger("McpRateLimiter");
|
|
29
19
|
|
|
30
20
|
readonly calls: number;
|
|
31
21
|
readonly windowMs: number;
|
|
32
22
|
readonly concurrent: number;
|
|
33
|
-
readonly #
|
|
34
|
-
readonly #
|
|
35
|
-
#sweptAt = 0;
|
|
36
|
-
#sharedFailed = false;
|
|
23
|
+
readonly #limiter: RateLimiter;
|
|
24
|
+
readonly #inFlight = new Map<string, number>();
|
|
37
25
|
|
|
38
26
|
constructor(
|
|
39
27
|
{
|
|
@@ -46,74 +34,37 @@ export class McpRateLimiter {
|
|
|
46
34
|
this.calls = Math.max(0, Math.floor(calls));
|
|
47
35
|
this.windowMs = Math.max(1, Math.floor(windowMs));
|
|
48
36
|
this.concurrent = Math.max(0, Math.floor(concurrent));
|
|
49
|
-
this.#
|
|
37
|
+
this.#limiter = new RateLimiter(shared, { topic: "mcpRate" });
|
|
50
38
|
}
|
|
51
39
|
|
|
52
40
|
async acquire(key: string, now = Date.now()): Promise<McpRateLimitVerdict> {
|
|
53
|
-
this.#
|
|
54
|
-
|
|
55
|
-
if (this.concurrent && bucket.inFlight >= this.concurrent)
|
|
56
|
-
return { ok: false, reason: "concurrent", retryAfterMs: 1_000 };
|
|
41
|
+
const held = this.#inFlight.get(key) ?? 0;
|
|
42
|
+
if (this.concurrent && held >= this.concurrent) return { ok: false, reason: "concurrent", retryAfterMs: 1_000 };
|
|
57
43
|
|
|
58
|
-
|
|
44
|
+
this.#inFlight.set(key, held + 1);
|
|
59
45
|
let released = false;
|
|
60
46
|
const release = () => {
|
|
61
47
|
if (released) return;
|
|
62
48
|
released = true;
|
|
63
|
-
|
|
49
|
+
const left = (this.#inFlight.get(key) ?? 1) - 1;
|
|
50
|
+
if (left > 0) this.#inFlight.set(key, left);
|
|
51
|
+
else this.#inFlight.delete(key);
|
|
64
52
|
};
|
|
65
|
-
|
|
66
|
-
|
|
53
|
+
if (!this.calls) return { ok: true, release };
|
|
54
|
+
const verdict = await this.#limiter.consume(key, { calls: this.calls, windowMs: this.windowMs }, now);
|
|
55
|
+
if (verdict.ok) return { ok: true, release };
|
|
67
56
|
release();
|
|
68
|
-
return { ok: false, reason: "calls", retryAfterMs };
|
|
57
|
+
return { ok: false, reason: "calls", retryAfterMs: verdict.retryAfterMs };
|
|
69
58
|
}
|
|
70
59
|
|
|
71
60
|
describe() {
|
|
72
|
-
const
|
|
61
|
+
const shared = this.#limiter.isShared;
|
|
73
62
|
const window = this.calls
|
|
74
|
-
? `${this.calls} calls / ${Math.round(this.windowMs / 1000)}s${across}`
|
|
63
|
+
? `${this.calls} calls / ${Math.round(this.windowMs / 1000)}s${shared ? " across instances" : ""}`
|
|
75
64
|
: "unlimited calls";
|
|
76
65
|
const inFlight = this.concurrent
|
|
77
|
-
? `${this.concurrent} in flight${
|
|
66
|
+
? `${this.concurrent} in flight${shared ? " per instance" : ""}`
|
|
78
67
|
: "unlimited in flight";
|
|
79
68
|
return `${window}, ${inFlight}, per caller`;
|
|
80
69
|
}
|
|
81
|
-
|
|
82
|
-
async #spend(key: string, bucket: McpRateBucket, now: number): Promise<number | null> {
|
|
83
|
-
if (this.#shared) {
|
|
84
|
-
const window = Math.floor(now / this.windowMs);
|
|
85
|
-
const end = (window + 1) * this.windowMs;
|
|
86
|
-
try {
|
|
87
|
-
const count = await this.#shared.incr("mcpRate", `${key}:${window}`, 1, {
|
|
88
|
-
expireAt: dayjs(end + this.windowMs),
|
|
89
|
-
});
|
|
90
|
-
this.#sharedFailed = false;
|
|
91
|
-
return count > this.calls ? Math.max(1, end - now) : null;
|
|
92
|
-
} catch (error) {
|
|
93
|
-
if (!this.#sharedFailed)
|
|
94
|
-
McpRateLimiter.#logger.warn(
|
|
95
|
-
`MCP rate limit cannot reach the cache, so each instance counts its own window until it can: ${String(error)}`,
|
|
96
|
-
);
|
|
97
|
-
this.#sharedFailed = true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
if (bucket.count >= this.calls) return Math.max(1, bucket.windowStart + this.windowMs - now);
|
|
101
|
-
bucket.count += 1;
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
#bucketOf(key: string, now: number) {
|
|
106
|
-
const found = this.#buckets.get(key);
|
|
107
|
-
if (found && now - found.windowStart < this.windowMs) return found;
|
|
108
|
-
const bucket: McpRateBucket = { windowStart: now, count: 0, inFlight: found?.inFlight ?? 0 };
|
|
109
|
-
this.#buckets.set(key, bucket);
|
|
110
|
-
return bucket;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
#sweep(now: number) {
|
|
114
|
-
if (now - this.#sweptAt < this.windowMs) return;
|
|
115
|
-
this.#sweptAt = now;
|
|
116
|
-
for (const [key, bucket] of this.#buckets)
|
|
117
|
-
if (!bucket.inFlight && now - bucket.windowStart >= this.windowMs) this.#buckets.delete(key);
|
|
118
|
-
}
|
|
119
70
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SignalContext } from "../../signal/signalContext";
|
|
1
2
|
import {
|
|
2
3
|
WEB_PROXY_RESULT,
|
|
3
4
|
type WebProxy,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
} from "./types";
|
|
10
11
|
|
|
11
12
|
export const copyBunRequestFields = (target: Request, source: Request): Request => {
|
|
13
|
+
SignalContext.carryClientIp(target, source);
|
|
12
14
|
const sourceWithParams = source as Request & { params?: Record<string, string> };
|
|
13
15
|
if (sourceWithParams.params) {
|
|
14
16
|
Object.defineProperty(target, "params", {
|
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
type DataInputOf,
|
|
11
11
|
DataLoader,
|
|
12
12
|
DocumentSchema,
|
|
13
|
+
type DocumentScopeOptions,
|
|
13
14
|
type DocumentUpdateInput,
|
|
15
|
+
type DocumentUpdateOptions,
|
|
14
16
|
documentQueryHelper,
|
|
15
17
|
type FindQueryOption,
|
|
16
18
|
getFilterMeta,
|
|
@@ -70,7 +72,7 @@ export class DatabaseResolver {
|
|
|
70
72
|
sort: resolveSort(queryOption?.sort),
|
|
71
73
|
skip: resolvePageSkip(queryOption?.skip),
|
|
72
74
|
|
|
73
|
-
limit: queryOption?.limit === undefined ? 0 : resolvePageLimit(queryOption.limit),
|
|
75
|
+
limit: queryOption?.limit === undefined || queryOption.limit === 0 ? 0 : resolvePageLimit(queryOption.limit),
|
|
74
76
|
select: queryOption?.select,
|
|
75
77
|
sample: queryOption?.sample,
|
|
76
78
|
});
|
|
@@ -198,9 +200,13 @@ export class DatabaseResolver {
|
|
|
198
200
|
function Model(this: any, data: Record<string, unknown>) {
|
|
199
201
|
return store.hydrate(data);
|
|
200
202
|
}
|
|
203
|
+
|
|
204
|
+
const scopeOf = (options?: DocumentScopeOptions) => (options?.withRemoved ? { withRemoved: true } : {});
|
|
205
|
+
const scopeArgs = (options?: DocumentScopeOptions): [] | [DocumentScopeOptions] =>
|
|
206
|
+
options?.withRemoved ? [{ withRemoved: true }] : [];
|
|
201
207
|
const createFindManyChain = (
|
|
202
208
|
query: QueryOf<any>,
|
|
203
|
-
options: { sort?: any; skip?: number; limit?: number; select?: any } = {},
|
|
209
|
+
options: { sort?: any; skip?: number; limit?: number; select?: any; withRemoved?: boolean } = {},
|
|
204
210
|
) => {
|
|
205
211
|
const chain: any = {
|
|
206
212
|
sort(sort: any) {
|
|
@@ -225,7 +231,10 @@ export class DatabaseResolver {
|
|
|
225
231
|
};
|
|
226
232
|
return chain;
|
|
227
233
|
};
|
|
228
|
-
const createFindOneChain = (
|
|
234
|
+
const createFindOneChain = (
|
|
235
|
+
query: QueryOf<any>,
|
|
236
|
+
options: { sort?: any; skip?: number; select?: any; withRemoved?: boolean } = {},
|
|
237
|
+
) => {
|
|
229
238
|
const chain: any = {
|
|
230
239
|
sort(sort: any) {
|
|
231
240
|
return createFindOneChain(query, { ...options, sort });
|
|
@@ -261,21 +270,27 @@ export class DatabaseResolver {
|
|
|
261
270
|
pickAndWrite: async (id: string, rawData: any) => await (await pickById(id)).set(rawData).save(),
|
|
262
271
|
pickOneAndWrite: async (query: QueryOf<any>, rawData: any) =>
|
|
263
272
|
await (await timedQuery(() => store.pickOne(query))).set(rawData).save(),
|
|
264
|
-
exists: async (query: QueryOf<any
|
|
273
|
+
exists: async (query: QueryOf<any>, options?: DocumentScopeOptions) =>
|
|
274
|
+
await timedQuery(() => store.exists(query, ...scopeArgs(options))),
|
|
265
275
|
sample: (query: QueryOf<any>, size = 1) => timedQuery(() => store.find(query, { sample: size, limit: size })),
|
|
266
276
|
sampleOne: (query: QueryOf<any>) => timedQuery(() => store.findOne(query, { sample: true })),
|
|
267
|
-
find: (query: QueryOf<any>, projection?: any
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
277
|
+
find: (query: QueryOf<any>, projection?: any, options?: DocumentScopeOptions) =>
|
|
278
|
+
createFindManyChain(query, { select: projection, ...scopeOf(options) }),
|
|
279
|
+
findOne: (query: QueryOf<any>, projection?: any, options?: DocumentScopeOptions) =>
|
|
280
|
+
createFindOneChain(query, { select: projection, ...scopeOf(options) }),
|
|
281
|
+
findById: (id: string | undefined, projection?: any, options?: DocumentScopeOptions) =>
|
|
282
|
+
id
|
|
283
|
+
? timedQuery(() => store.findOne({ id }, { select: projection, ...scopeOf(options) }))
|
|
284
|
+
: Promise.resolve(null),
|
|
285
|
+
count: (query: QueryOf<any>, options?: DocumentScopeOptions) =>
|
|
286
|
+
timedQuery(() => store.count(query, ...scopeArgs(options))),
|
|
287
|
+
updateOne: (query: QueryOf<any>, update: DocumentUpdateInput, options?: DocumentUpdateOptions) =>
|
|
273
288
|
timedQuery(() => store.updateOneByQuery(query, update, options)),
|
|
274
|
-
updateMany: (query: QueryOf<any>, update: DocumentUpdateInput) =>
|
|
275
|
-
timedQuery(() => store.updateManyByQuery(query, update)),
|
|
289
|
+
updateMany: (query: QueryOf<any>, update: DocumentUpdateInput, options?: DocumentScopeOptions) =>
|
|
290
|
+
timedQuery(() => store.updateManyByQuery(query, update, ...scopeArgs(options))),
|
|
276
291
|
removeOne: (query: QueryOf<any>) => timedQuery(() => store.removeOneByQuery(query)),
|
|
277
292
|
removeMany: (query: QueryOf<any>) => timedQuery(() => store.removeManyByQuery(query)),
|
|
278
|
-
updateById: (id: string, update: DocumentUpdateInput, options?:
|
|
293
|
+
updateById: (id: string, update: DocumentUpdateInput, options?: DocumentUpdateOptions) =>
|
|
279
294
|
timedQuery(() => store.updateOneByQuery({ id }, update, options)),
|
|
280
295
|
removeById: (id: string) => timedQuery(() => store.removeOneByQuery({ id })),
|
|
281
296
|
|
|
@@ -15,7 +15,14 @@ import {
|
|
|
15
15
|
SLICE_META,
|
|
16
16
|
} from "akanjs/base";
|
|
17
17
|
import { capitalize, cookieHeaderHasAuthToken, Logger } from "akanjs/common";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
type ConstantField,
|
|
20
|
+
deserialize,
|
|
21
|
+
MAX_PAGE_SIZE,
|
|
22
|
+
resolvePageLimit,
|
|
23
|
+
resolvePageSkip,
|
|
24
|
+
serialize,
|
|
25
|
+
} from "akanjs/constant";
|
|
19
26
|
import { baseDocumentColumns, documentQueryHelper, getFilterSortByKey, type QueryFieldMap } from "akanjs/document";
|
|
20
27
|
import {
|
|
21
28
|
type AkanJob,
|
|
@@ -30,6 +37,7 @@ import {
|
|
|
30
37
|
import { websocketRoomContract } from "../../common/websocketContract";
|
|
31
38
|
import { type Endpoint, type EndpointCls, sliceEndpoint } from "../../signal/endpoint";
|
|
32
39
|
import type { EndpointInfo } from "../../signal/endpointInfo";
|
|
40
|
+
import { EndpointRateLimit } from "../../signal/endpointRateLimit";
|
|
33
41
|
import type { Internal, InternalCls } from "../../signal/internal";
|
|
34
42
|
import type { InternalInfo } from "../../signal/internalInfo";
|
|
35
43
|
import type { MiddlewareCls } from "../../signal/middleware";
|
|
@@ -65,6 +73,7 @@ export class SignalResolver {
|
|
|
65
73
|
};
|
|
66
74
|
static readonly #coalescingRooms = new Set<string>();
|
|
67
75
|
static readonly #liveRoutes = new Map<string, { store: DocumentStore; route: LiveRoute }>();
|
|
76
|
+
static readonly #everyRowAsked = new Set<string>();
|
|
68
77
|
|
|
69
78
|
static coalescesRoom(roomId: string): boolean {
|
|
70
79
|
const separator = roomId.indexOf("-");
|
|
@@ -325,6 +334,17 @@ export class SignalResolver {
|
|
|
325
334
|
else SignalResolver.logger.warn(message);
|
|
326
335
|
}
|
|
327
336
|
|
|
337
|
+
static #clientPageLimit(listKey: string, asked: unknown) {
|
|
338
|
+
if (asked !== 0) return resolvePageLimit(asked);
|
|
339
|
+
if (!SignalResolver.#everyRowAsked.has(listKey)) {
|
|
340
|
+
SignalResolver.#everyRowAsked.add(listKey);
|
|
341
|
+
SignalResolver.logger.warn(
|
|
342
|
+
`${listKey} was asked for limit: 0 and served ${MAX_PAGE_SIZE} rows, the most a client may take; paginate, or name a page size`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return MAX_PAGE_SIZE;
|
|
346
|
+
}
|
|
347
|
+
|
|
328
348
|
static #getJobArgs(key: string, internalInfo: InternalInfo, job: AkanJob): unknown[] {
|
|
329
349
|
const data = Array.isArray(job.data) ? (job.data as unknown[]) : job.data === undefined ? [] : [job.data];
|
|
330
350
|
return internalInfo.args.map((arg, idx) =>
|
|
@@ -371,7 +391,7 @@ export class SignalResolver {
|
|
|
371
391
|
.exec(async function (this: any, ...requestArgs: any) {
|
|
372
392
|
const args = requestArgs.slice(0, argLength);
|
|
373
393
|
const skip = resolvePageSkip(requestArgs[argLength]);
|
|
374
|
-
const limit =
|
|
394
|
+
const limit = SignalResolver.#clientPageLimit(listKey, requestArgs[argLength + 1]);
|
|
375
395
|
const sort = requestArgs[argLength + 2] ?? "latest";
|
|
376
396
|
const internalArgs = requestArgs.slice(argLength + 3);
|
|
377
397
|
const query = assertSliceQuery(
|
|
@@ -437,41 +457,46 @@ export class SignalResolver {
|
|
|
437
457
|
});
|
|
438
458
|
});
|
|
439
459
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
460
|
+
if (sliceCls.crud.get) {
|
|
461
|
+
endpointObj[refName] = (builder as any)
|
|
462
|
+
.query(cnst.full, { guards: sliceCls.getGuards })
|
|
463
|
+
.param(`${refName}Id`, ID)
|
|
464
|
+
.exec(async function (this: any, id: string) {
|
|
465
|
+
return await this[serviceName][`get${capitalizedRefName}`](id);
|
|
466
|
+
});
|
|
446
467
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
468
|
+
endpointObj[`light${capitalizedRefName}`] = (builder as any)
|
|
469
|
+
.query(cnst.light, { guards: sliceCls.getGuards })
|
|
470
|
+
.param(`${refName}Id`, ID)
|
|
471
|
+
.exec(async function (this: any, id: string) {
|
|
472
|
+
return await this[serviceName][`get${capitalizedRefName}`](id);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
453
475
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
476
|
+
if (sliceCls.crud.create)
|
|
477
|
+
endpointObj[`create${capitalizedRefName}`] = (builder as any)
|
|
478
|
+
.mutation(cnst.full, { guards: sliceCls.createGuards })
|
|
479
|
+
.body("data", cnst.input)
|
|
480
|
+
.exec(async function (this: any, data: any) {
|
|
481
|
+
return await this[serviceName].__create(data);
|
|
482
|
+
});
|
|
460
483
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
484
|
+
if (sliceCls.crud.update)
|
|
485
|
+
endpointObj[`update${capitalizedRefName}`] = (builder as any)
|
|
486
|
+
.mutation(cnst.full, { guards: sliceCls.updateGuards })
|
|
487
|
+
.param(`${refName}Id`, ID)
|
|
488
|
+
.body("data", cnst.input)
|
|
489
|
+
.exec(async function (this: any, id: string, data: any) {
|
|
490
|
+
return await this[serviceName].__update(id, data);
|
|
491
|
+
});
|
|
468
492
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
493
|
+
if (sliceCls.crud.remove)
|
|
494
|
+
endpointObj[`remove${capitalizedRefName}`] = (builder as any)
|
|
495
|
+
.mutation(cnst.full, { guards: sliceCls.removeGuards })
|
|
496
|
+
.param(`${refName}Id`, ID)
|
|
497
|
+
.exec(async function (this: any, id: string) {
|
|
498
|
+
return await this[serviceName].__remove(id);
|
|
499
|
+
});
|
|
475
500
|
return endpointObj;
|
|
476
501
|
}) {}
|
|
477
502
|
return SliceEndpoint;
|
|
@@ -600,6 +625,7 @@ export class SignalResolver {
|
|
|
600
625
|
const routeOptions: NonNullable<SignalRoutes["routeOptions"]> = {};
|
|
601
626
|
const wsRoutes: WebsocketRoutes = {};
|
|
602
627
|
const defaultPrefix = endpointCls.srv.cnst?.refName;
|
|
628
|
+
SignalResolver.#warnUnguarded(endpointMeta);
|
|
603
629
|
Object.entries(endpointMeta).forEach(([key, endpointInfo]) => {
|
|
604
630
|
const path = endpointInfo.getRoutePath(key, defaultPrefix);
|
|
605
631
|
if (endpointInfo.signalOption.globalPrefix !== undefined) {
|
|
@@ -627,7 +653,7 @@ export class SignalResolver {
|
|
|
627
653
|
SignalResolver.#mountHttpRoute(
|
|
628
654
|
routes,
|
|
629
655
|
path,
|
|
630
|
-
SignalResolver.#canUsePrimitiveQueryFastPath(endpointInfo, middleware)
|
|
656
|
+
SignalResolver.#canUsePrimitiveQueryFastPath(key, endpointInfo, middleware)
|
|
631
657
|
? {
|
|
632
658
|
GET: async (req) => {
|
|
633
659
|
if (req.headers.get("authorization") || cookieHeaderHasAuthToken(req.headers.get("cookie")))
|
|
@@ -733,6 +759,15 @@ export class SignalResolver {
|
|
|
733
759
|
return { routes, wsRoutes, routeOptions };
|
|
734
760
|
}
|
|
735
761
|
|
|
762
|
+
static #warnUnguarded(endpointMeta: { [key: string]: EndpointInfo }) {
|
|
763
|
+
const open = Object.entries(endpointMeta)
|
|
764
|
+
.filter(([, endpointInfo]) => !endpointInfo.signalOption.guards?.length)
|
|
765
|
+
.map(([key]) => key);
|
|
766
|
+
if (!open.length) return;
|
|
767
|
+
SignalResolver.logger.warn(
|
|
768
|
+
`${open.join(", ")} declare${open.length === 1 ? "s" : ""} no guards, so anyone can call ${open.length === 1 ? "it" : "them"} over HTTP and WebSocket. Name the guards, or \`guards: [Public]\` to keep one open on purpose.`,
|
|
769
|
+
);
|
|
770
|
+
}
|
|
736
771
|
static #selectCache = new WeakMap<Cls, Record<string, true>>();
|
|
737
772
|
static #selectForConstant(constant: Cls): Record<string, true> | undefined {
|
|
738
773
|
const cached = SignalResolver.#selectCache.get(constant);
|
|
@@ -744,9 +779,14 @@ export class SignalResolver {
|
|
|
744
779
|
return select;
|
|
745
780
|
}
|
|
746
781
|
|
|
747
|
-
static #canUsePrimitiveQueryFastPath(
|
|
782
|
+
static #canUsePrimitiveQueryFastPath(
|
|
783
|
+
key: string,
|
|
784
|
+
endpointInfo: EndpointInfo,
|
|
785
|
+
middleware: Map<string, MiddlewareCls>,
|
|
786
|
+
) {
|
|
748
787
|
return (
|
|
749
788
|
process.env.AKAN_TRACE !== "1" &&
|
|
789
|
+
!EndpointRateLimit.budgetOf(key, endpointInfo) &&
|
|
750
790
|
endpointInfo.args.length === 0 &&
|
|
751
791
|
endpointInfo.internalArgs.length === 0 &&
|
|
752
792
|
(endpointInfo.signalOption.guards?.length ?? 0) === 0 &&
|
package/server/robots.ts
CHANGED
|
@@ -2,33 +2,11 @@ import { getApiPrefix } from "akanjs/base";
|
|
|
2
2
|
|
|
3
3
|
const DEFAULT_DISALLOW_PATHS = ["/_akan", "/admin", "/manager", "/private"] as const;
|
|
4
4
|
|
|
5
|
-
const DEFAULT_AI_CRAWLERS = [
|
|
6
|
-
"GPTBot",
|
|
7
|
-
"ChatGPT-User",
|
|
8
|
-
"CCBot",
|
|
9
|
-
"ClaudeBot",
|
|
10
|
-
"Claude-Web",
|
|
11
|
-
"anthropic-ai",
|
|
12
|
-
"Google-Extended",
|
|
13
|
-
"PerplexityBot",
|
|
14
|
-
"Perplexity-User",
|
|
15
|
-
"Bytespider",
|
|
16
|
-
"Amazonbot",
|
|
17
|
-
"Applebot-Extended",
|
|
18
|
-
"Meta-ExternalAgent",
|
|
19
|
-
"FacebookBot",
|
|
20
|
-
"Diffbot",
|
|
21
|
-
"Omgilibot",
|
|
22
|
-
"Omgili",
|
|
23
|
-
] as const;
|
|
24
|
-
|
|
25
5
|
export function createDefaultRobotsTxt(): string {
|
|
26
6
|
const lines = [
|
|
27
7
|
"User-agent: *",
|
|
28
8
|
"Allow: /",
|
|
29
9
|
...[getApiPrefix(), ...DEFAULT_DISALLOW_PATHS].map((path) => `Disallow: ${path}`),
|
|
30
|
-
"",
|
|
31
|
-
...DEFAULT_AI_CRAWLERS.flatMap((crawler) => [`User-agent: ${crawler}`, "Disallow: /", ""]),
|
|
32
10
|
];
|
|
33
|
-
return `${lines.join("\n")
|
|
11
|
+
return `${lines.join("\n")}\n`;
|
|
34
12
|
}
|
|
@@ -171,7 +171,6 @@ export class RouteTreeBuilder {
|
|
|
171
171
|
: {
|
|
172
172
|
renderPage: routeRender,
|
|
173
173
|
pageIncludesOwnLayout: parsed.leaf === "_index",
|
|
174
|
-
isSpecialRoute: parsed.isSpecialRoute,
|
|
175
174
|
}),
|
|
176
175
|
} as Route);
|
|
177
176
|
}
|
|
@@ -198,7 +197,6 @@ export class RouteTreeBuilder {
|
|
|
198
197
|
renderRootLayouts: layer.pageRenderRootLayouts,
|
|
199
198
|
renderLayouts: layer.pageRenderLayouts,
|
|
200
199
|
resolveHead: RouteTreeBuilder.#composeHeadResolvers(route.renderPage.resolveHead, pageHead),
|
|
201
|
-
isSpecialRoute: route.isSpecialRoute,
|
|
202
200
|
pageState: route.pageState ?? defaultPageState,
|
|
203
201
|
},
|
|
204
202
|
]
|
|
@@ -301,6 +299,10 @@ export class RouteTreeBuilder {
|
|
|
301
299
|
routeRender.getRouteDefinition = async () => (await loadModule()).definition;
|
|
302
300
|
} else {
|
|
303
301
|
routeRender.getLayoutPageConfig = pageConfigOf;
|
|
302
|
+
routeRender.getLayoutTheme = async () => {
|
|
303
|
+
const { module: mod } = await loadModule();
|
|
304
|
+
return "theme" in mod ? (mod.theme as string | undefined) : undefined;
|
|
305
|
+
};
|
|
304
306
|
routeRender.resolveNotFound = async () => syncFallbacks((await loadModule()).module as LayoutModule).NotFound;
|
|
305
307
|
routeRender.resolveError = async () => syncFallbacks((await loadModule()).module as LayoutModule).Error;
|
|
306
308
|
}
|