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.
Files changed (127) hide show
  1. package/CHANGELOG.md +3670 -0
  2. package/base/base.ts +1 -1
  3. package/client/cookie.ts +4 -6
  4. package/client/csrTypes.ts +2 -2
  5. package/common/Logger.ts +10 -0
  6. package/common/index.ts +0 -1
  7. package/common/routeConvention.ts +1 -9
  8. package/dictionary/base.dictionary.ts +7 -0
  9. package/dictionary/trans.ts +7 -1
  10. package/document/documentQuery.ts +6 -1
  11. package/document/index.ts +1 -0
  12. package/document/into.ts +15 -7
  13. package/document/liveRowScope.ts +79 -0
  14. package/fetch/client/fetchClient.ts +64 -12
  15. package/fetch/client/httpClient.ts +13 -4
  16. package/index.ts +22 -0
  17. package/package.json +1 -1
  18. package/server/akanOption.ts +20 -1
  19. package/server/akanServer.ts +5 -0
  20. package/server/cachePolicy.ts +12 -2
  21. package/server/crawler.ts +6 -0
  22. package/server/head.tsx +0 -4
  23. package/server/mcp/McpAuth.ts +3 -2
  24. package/server/mcp/McpExecutionContext.ts +15 -14
  25. package/server/mcp/McpRateLimiter.ts +18 -67
  26. package/server/proxy/webProxyRunner.ts +2 -0
  27. package/server/resolver/database.resolver.ts +28 -13
  28. package/server/resolver/signal.resolver.ts +75 -35
  29. package/server/robots.ts +1 -23
  30. package/server/routeTreeBuilder.ts +4 -2
  31. package/server/rscWorker.tsx +12 -24
  32. package/server/rscWorkerHost.ts +16 -7
  33. package/server/ssrFromRscRenderer.tsx +19 -15
  34. package/server/ssrTypes.ts +9 -0
  35. package/server/webRouter.ts +9 -2
  36. package/service/index.ts +1 -0
  37. package/service/predefinedAdaptor/index.ts +1 -0
  38. package/service/predefinedAdaptor/rateLimit.adaptor.ts +15 -0
  39. package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +119 -27
  40. package/service/predefinedAdaptor/sql/types.ts +14 -12
  41. package/service/rateLimiter.ts +80 -0
  42. package/signal/base.signal.ts +7 -6
  43. package/signal/endpointRateLimit.ts +159 -0
  44. package/signal/index.ts +1 -0
  45. package/signal/middleware.ts +6 -5
  46. package/signal/serializer/fetch.serializer.ts +15 -4
  47. package/signal/signalContext.ts +148 -67
  48. package/signal/slice.ts +30 -9
  49. package/signal/types.ts +8 -0
  50. package/test/signalTestRuntime.ts +2 -1
  51. package/types/base/base.d.ts +1 -1
  52. package/types/client/cookie.d.ts +1 -1
  53. package/types/client/csrTypes.d.ts +2 -2
  54. package/types/common/Logger.d.ts +2 -0
  55. package/types/common/index.d.ts +1 -1
  56. package/types/common/routeConvention.d.ts +0 -2
  57. package/types/dictionary/base.dictionary.d.ts +1 -1
  58. package/types/dictionary/dictionary.d.ts +8 -8
  59. package/types/document/documentQuery.d.ts +5 -1
  60. package/types/document/index.d.ts +1 -0
  61. package/types/document/into.d.ts +7 -7
  62. package/types/document/liveRowScope.d.ts +9 -0
  63. package/types/fetch/client/fetchClient.d.ts +9 -1
  64. package/types/index.d.ts +32 -0
  65. package/types/server/akanOption.d.ts +7 -1
  66. package/types/server/cachePolicy.d.ts +2 -1
  67. package/types/server/crawler.d.ts +1 -0
  68. package/types/server/head.d.ts +0 -3
  69. package/types/server/mcp/McpExecutionContext.d.ts +4 -1
  70. package/types/server/mcp/McpRateLimiter.d.ts +2 -6
  71. package/types/server/rscWorkerHost.d.ts +3 -0
  72. package/types/server/ssrFromRscRenderer.d.ts +3 -1
  73. package/types/server/ssrTypes.d.ts +8 -0
  74. package/types/service/index.d.ts +1 -0
  75. package/types/service/predefinedAdaptor/index.d.ts +1 -0
  76. package/types/service/predefinedAdaptor/rateLimit.adaptor.d.ts +10 -0
  77. package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +6 -10
  78. package/types/service/predefinedAdaptor/sql/types.d.ts +13 -17
  79. package/types/service/rateLimiter.d.ts +24 -0
  80. package/types/signal/endpointRateLimit.d.ts +37 -0
  81. package/types/signal/index.d.ts +1 -0
  82. package/types/signal/signalContext.d.ts +33 -15
  83. package/types/signal/slice.d.ts +17 -5
  84. package/types/signal/types.d.ts +8 -0
  85. package/types/test/signalTestRuntime.d.ts +1 -1
  86. package/types/ui/recipe/badgeRecipe.d.ts +2 -2
  87. package/types/ui/recipe/buttonRecipe.d.ts +4 -4
  88. package/types/ui/recipe/inputRecipe.d.ts +3 -3
  89. package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +33 -0
  90. package/types/vendor/@akanjs/native/packages/cli/src/lib/bplist.d.ts +11 -0
  91. package/types/vendor/@akanjs/native/packages/cli/src/lib/dmg.d.ts +59 -0
  92. package/types/vendor/@akanjs/native/packages/cli/src/lib/dsStore.d.ts +26 -0
  93. package/types/vendor/@akanjs/native/packages/cli/src/lib/macAlias.d.ts +24 -0
  94. package/types/vendor/@akanjs/native/packages/cli/src/lib/macossigning.d.ts +0 -1
  95. package/types/vendor/@akanjs/native/packages/cli/src/lib/project.d.ts +3 -1
  96. package/types/vendor/@akanjs/native/packages/cli/src/platforms/linux-appimage.d.ts +6 -1
  97. package/types/vendor/@akanjs/native/packages/desktop/src/deeplinks.d.ts +9 -2
  98. package/types/webkit/RenderFailure.d.ts +6 -0
  99. package/types/webkit/useFrameRuntime.d.ts +0 -1
  100. package/ui/Input.tsx +7 -1
  101. package/ui/recipe/buttonRecipe.ts +1 -0
  102. package/ui/recipe/inputRecipe.ts +13 -7
  103. package/ui/styles.css +1 -0
  104. package/vendor/@akanjs/native/native/desktop/src/lib.rs +8 -1
  105. package/vendor/@akanjs/native/native/macos/dmg/background.html +18 -0
  106. package/vendor/@akanjs/native/native/macos/dmg/background.png +0 -0
  107. package/vendor/@akanjs/native/native/macos/dmg/background@2x.png +0 -0
  108. package/vendor/@akanjs/native/packages/cli/src/config.ts +23 -0
  109. package/vendor/@akanjs/native/packages/cli/src/lib/addons.ts +2 -1
  110. package/vendor/@akanjs/native/packages/cli/src/lib/bplist.ts +91 -0
  111. package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +9 -0
  112. package/vendor/@akanjs/native/packages/cli/src/lib/dmg.ts +331 -0
  113. package/vendor/@akanjs/native/packages/cli/src/lib/dsStore.ts +191 -0
  114. package/vendor/@akanjs/native/packages/cli/src/lib/exec.ts +11 -1
  115. package/vendor/@akanjs/native/packages/cli/src/lib/macAlias.ts +101 -0
  116. package/vendor/@akanjs/native/packages/cli/src/lib/macossigning.ts +1 -28
  117. package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +6 -1
  118. package/vendor/@akanjs/native/packages/cli/src/platforms/linux-appimage.ts +17 -7
  119. package/vendor/@akanjs/native/packages/cli/src/platforms/linux.ts +6 -1
  120. package/vendor/@akanjs/native/packages/cli/src/platforms/macos.ts +5 -2
  121. package/vendor/@akanjs/native/packages/desktop/src/deeplinks.ts +33 -10
  122. package/vendor/@akanjs/native/packages/desktop/src/host.ts +1 -1
  123. package/vendor/use-agentic/CHANGELOG.md +663 -0
  124. package/webkit/CsrRouteTable.ts +0 -2
  125. package/webkit/RenderFailure.tsx +32 -0
  126. package/webkit/RenderLayer.tsx +22 -2
  127. package/webkit/useCsrValues.ts +21 -11
@@ -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 { entry: createRouteCacheEntry({ request: input.request, url: input.url, theme: input.theme, ttl }) };
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
  }
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { PromiseOrObject } from "akanjs/base";
3
- import { clientAddressFromHeaders } from "akanjs/common";
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
- if (!token) return `ip:${clientAddressFromHeaders(req.headers) ?? "anonymous"}`;
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 getArgs(endpointInfo: EndpointInfo): Promise<unknown[]> {
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
- const value = McpExecutionContext.#lift(arg, this.#arguments[arg.name] ?? null);
28
- try {
29
- return deserialize(arg.argRef, arg.arrDepth, value, {
30
- key: arg.name,
31
- nullable: arg.option?.nullable,
32
- enum: arg.enum,
33
- });
34
- } catch {
35
-
36
- throw new McpArgumentError(McpExecutionContext.#argumentMessage(arg, value));
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 Dayjs, dayjs } from "akanjs/base";
2
- import { Logger } from "akanjs/common";
1
+ import { type RateCounter, RateLimiter } from "akanjs/service";
3
2
 
4
- export interface McpSharedCounter {
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 #shared: McpSharedCounter | null;
34
- readonly #buckets = new Map<string, McpRateBucket>();
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.#shared = shared;
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.#sweep(now);
54
- const bucket = this.#bucketOf(key, now);
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
- bucket.inFlight += 1;
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
- bucket.inFlight = Math.max(0, bucket.inFlight - 1);
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
- const retryAfterMs = this.calls ? await this.#spend(key, bucket, now) : null;
66
- if (retryAfterMs === null) return { ok: true, release };
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 across = this.#shared ? " across instances" : "";
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${this.#shared ? " per instance" : ""}`
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 = (query: QueryOf<any>, options: { sort?: any; skip?: number; select?: any } = {}) => {
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>) => await timedQuery(() => store.exists(query)),
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) => createFindManyChain(query, { select: projection }),
268
- findOne: (query: QueryOf<any>, projection?: any) => createFindOneChain(query, { select: projection }),
269
- findById: (id: string | undefined, projection?: any) =>
270
- id ? timedQuery(() => store.findOne({ id }, { select: projection })) : Promise.resolve(null),
271
- count: (query: QueryOf<any>) => timedQuery(() => store.count(query)),
272
- updateOne: (query: QueryOf<any>, update: DocumentUpdateInput, options?: { upsert?: boolean }) =>
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?: { upsert?: boolean }) =>
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 { type ConstantField, deserialize, resolvePageLimit, resolvePageSkip, serialize } from "akanjs/constant";
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 = resolvePageLimit(requestArgs[argLength + 1]);
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
- endpointObj[refName] = (builder as any)
441
- .query(cnst.full, { guards: sliceCls.getGuards })
442
- .param(`${refName}Id`, ID)
443
- .exec(async function (this: any, id: string) {
444
- return await this[serviceName][`get${capitalizedRefName}`](id);
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
- endpointObj[`light${capitalizedRefName}`] = (builder as any)
448
- .query(cnst.light, { guards: sliceCls.getGuards })
449
- .param(`${refName}Id`, ID)
450
- .exec(async function (this: any, id: string) {
451
- return await this[serviceName][`get${capitalizedRefName}`](id);
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
- endpointObj[`create${capitalizedRefName}`] = (builder as any)
455
- .mutation(cnst.full, { guards: sliceCls.createGuards })
456
- .body("data", cnst.input)
457
- .exec(async function (this: any, data: any) {
458
- return await this[serviceName].__create(data);
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
- endpointObj[`update${capitalizedRefName}`] = (builder as any)
462
- .mutation(cnst.full, { guards: sliceCls.updateGuards })
463
- .param(`${refName}Id`, ID)
464
- .body("data", cnst.input)
465
- .exec(async function (this: any, id: string, data: any) {
466
- return await this[serviceName].__update(id, data);
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
- endpointObj[`remove${capitalizedRefName}`] = (builder as any)
470
- .mutation(cnst.full, { guards: sliceCls.removeGuards })
471
- .param(`${refName}Id`, ID)
472
- .exec(async function (this: any, id: string) {
473
- return await this[serviceName].__remove(id);
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(endpointInfo: EndpointInfo, middleware: Map<string, MiddlewareCls>) {
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").trimEnd()}\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
  }