akanjs 2.4.1 → 2.4.2-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/client/csrTypes.ts +7 -0
  2. package/client/frameConfig.ts +6 -1
  3. package/client/rscNavigation.ts +9 -0
  4. package/common/index.ts +5 -0
  5. package/common/websocketAuth.ts +24 -0
  6. package/constant/fieldInfo.ts +4 -3
  7. package/constant/index.ts +2 -0
  8. package/constant/textFieldPathSet.ts +8 -0
  9. package/constant/textFieldPaths.ts +59 -0
  10. package/constant/types.ts +0 -4
  11. package/constant/via.ts +13 -28
  12. package/dictionary/dictInfo.ts +4 -0
  13. package/document/documentQuery.ts +17 -1
  14. package/document/documentSchema.ts +1 -15
  15. package/document/filterMeta.ts +4 -2
  16. package/fetch/client/fetchClient.ts +1 -0
  17. package/fetch/client/wsClient.ts +28 -1
  18. package/index.ts +6 -0
  19. package/local/apps/serverLifecycle/serverLifecycle-local.db +0 -0
  20. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  21. package/local/apps/serverLifecycle/serverLifecycle-local.db-wal +0 -0
  22. package/package.json +1 -1
  23. package/server/akanServer.ts +5 -4
  24. package/server/devtools/types.ts +2 -2
  25. package/server/resolver/database.resolver.ts +5 -6
  26. package/server/resolver/signal.resolver.ts +23 -0
  27. package/server/routing/apiRouter.ts +11 -3
  28. package/server/routing/appWsData.ts +50 -0
  29. package/server/rscClient.tsx +9 -0
  30. package/service/predefinedAdaptor/database.adaptor.ts +212 -88
  31. package/service/predefinedAdaptor/index.ts +1 -0
  32. package/service/predefinedAdaptor/searchIndex.ts +517 -0
  33. package/service/predefinedAdaptor/sqlDescriptor.ts +25 -0
  34. package/signal/signalContext.ts +37 -12
  35. package/signal/types.ts +2 -1
  36. package/types/client/csrTypes.d.ts +7 -0
  37. package/types/client/rscNavigation.d.ts +8 -0
  38. package/types/common/index.d.ts +1 -0
  39. package/types/common/websocketAuth.d.ts +19 -0
  40. package/types/constant/fieldInfo.d.ts +4 -3
  41. package/types/constant/index.d.ts +2 -0
  42. package/types/constant/textFieldPathSet.d.ts +8 -0
  43. package/types/constant/textFieldPaths.d.ts +10 -0
  44. package/types/constant/types.d.ts +0 -3
  45. package/types/constant/via.d.ts +4 -24
  46. package/types/dictionary/base.dictionary.d.ts +1 -1
  47. package/types/dictionary/dictionary.d.ts +8 -8
  48. package/types/document/documentQuery.d.ts +13 -1
  49. package/types/document/documentSchema.d.ts +0 -3
  50. package/types/document/filterMeta.d.ts +2 -1
  51. package/types/fetch/client/wsClient.d.ts +6 -0
  52. package/types/index.d.ts +6 -0
  53. package/types/server/devtools/types.d.ts +2 -2
  54. package/types/server/resolver/signal.resolver.d.ts +6 -0
  55. package/types/server/routing/apiRouter.d.ts +3 -4
  56. package/types/server/routing/appWsData.d.ts +24 -0
  57. package/types/server/rscClient.d.ts +1 -0
  58. package/types/service/predefinedAdaptor/database.adaptor.d.ts +46 -13
  59. package/types/service/predefinedAdaptor/index.d.ts +1 -0
  60. package/types/service/predefinedAdaptor/searchIndex.d.ts +73 -0
  61. package/types/service/predefinedAdaptor/sqlDescriptor.d.ts +5 -0
  62. package/types/signal/signalContext.d.ts +7 -0
  63. package/types/signal/types.d.ts +5 -1
  64. package/types/ui/Constant/schemaDoc.d.ts +2 -2
  65. package/ui/Constant/schemaDoc.ts +8 -2
  66. package/ui/Model/EditModal.tsx +24 -6
@@ -1,11 +1,12 @@
1
1
  import { dayjs } from "akanjs/base";
2
- import type { Logger } from "akanjs/common";
2
+ import { type Logger, websocketAuthContract } from "akanjs/common";
3
3
  import type { InjectRegistry } from "akanjs/service";
4
4
  import { Exception, type WebsocketReqData } from "akanjs/signal";
5
5
  import type { HmrWsData, HmrWsHub } from "../hmr/wsHub";
6
6
  import { copyBunRequestFields, type WebProxyRunner } from "../proxy";
7
7
  import { SignalResolver } from "../resolver";
8
8
  import type { HttpRoutes, SignalRouteOptions, WebsocketRoutes } from "../types";
9
+ import { AppWsData } from "./appWsData";
9
10
 
10
11
  /**
11
12
  * Minimal render-state view the HMR WS hello message needs.
@@ -30,7 +31,7 @@ export interface ApiRouteInputs {
30
31
  routeOptions?: Record<string, SignalRouteOptions>;
31
32
  renderEnvRoutes: HttpRoutes;
32
33
  /** Upgrades the incoming request into an app-signal WebSocket. */
33
- upgradeAppWs: (req: Request, data: { createdAt: number }) => boolean;
34
+ upgradeAppWs: (req: Request, data: AppWsData) => boolean;
34
35
  webProxyRunner?: WebProxyRunner | null;
35
36
  }
36
37
 
@@ -85,7 +86,7 @@ export class ApiRouter {
85
86
  const endpointPaths = new Set([...endpointEntries.map(([path]) => path), ...builtinEntries.map(([path]) => path)]);
86
87
  const routeTable = {
87
88
  [`${prefix}${websocketPrefix}` as "/api/ws"]: (req) => {
88
- const upgraded = upgradeAppWs(req, { createdAt: Date.now() });
89
+ const upgraded = upgradeAppWs(req, AppWsData.fromRequest(req));
89
90
  if (upgraded) return;
90
91
  return new Response("Failed to upgrade to WebSocket", { status: 500 });
91
92
  },
@@ -138,6 +139,13 @@ export class ApiRouter {
138
139
  if (typeof message === "string") {
139
140
  const msg = JSON.parse(message) as WebsocketReqData;
140
141
  if (!msg.key) throw new Error("Message key is required");
142
+ if (msg.key === websocketAuthContract.key) {
143
+
144
+ AppWsData.applyCredential(AppWsData.of(ws), websocketAuthContract.readJwt(msg.data));
145
+ const revokedRooms = await SignalResolver.revalidateWsRooms(ws, registry);
146
+ ws.send(JSON.stringify(websocketAuthContract.makeAck(revokedRooms)));
147
+ return;
148
+ }
141
149
  const wsRoute = wsRoutes[msg.key];
142
150
  if (!wsRoute) throw new Error(`WebSocket route "${msg.key}" is not registered`);
143
151
  const eventType =
@@ -0,0 +1,50 @@
1
+ const CREDENTIAL_HEADERS = ["authorization", "cookie", "user-agent"] as const;
2
+
3
+ /**
4
+ * Credential snapshot taken at the websocket handshake and carried on `ws.data` for the life of the
5
+ * socket, so auth middleware and guards can read the caller the same way they read an HTTP request.
6
+ * Only the credential headers are copied — retaining the whole `Request` would pin it for as long
7
+ * as the socket stays open.
8
+ */
9
+ export class AppWsData {
10
+ static fromRequest(req: Request): AppWsData {
11
+ const headers = new Headers();
12
+ for (const key of CREDENTIAL_HEADERS) {
13
+ const value = req.headers.get(key);
14
+ if (value) headers.set(key, value);
15
+ }
16
+ return new AppWsData(headers);
17
+ }
18
+ static of(ws: Bun.ServerWebSocket<unknown>): AppWsData {
19
+ return ws.data as AppWsData;
20
+ }
21
+ /**
22
+ * Swaps the credential the socket authenticates with. Callers must run this synchronously on the
23
+ * auth frame: frames arrive in order, so a subscribe sent right after the credential must not be
24
+ * able to observe the previous one.
25
+ */
26
+ static applyCredential(data: AppWsData, jwt: string | null) {
27
+ if (jwt) data.headers.set("authorization", `Bearer ${jwt}`);
28
+ else {
29
+ data.headers.delete("authorization");
30
+ data.cookies.delete("jwt");
31
+ const cookie = [...data.cookies].map(([name, value]) => `${name}=${value}`).join("; ");
32
+ if (cookie) data.headers.set("cookie", cookie);
33
+ else data.headers.delete("cookie");
34
+ }
35
+ data.account = undefined;
36
+ data.resolvedAuthorization = undefined;
37
+ }
38
+ createdAt: number;
39
+ headers: Headers;
40
+ cookies: Bun.CookieMap;
41
+ account?: unknown;
42
+ /** The `authorization` value `account` was resolved from, so each frame need not re-verify it. */
43
+ resolvedAuthorization?: string;
44
+ socketId?: string;
45
+ constructor(headers: Headers) {
46
+ this.createdAt = Date.now();
47
+ this.headers = headers;
48
+ this.cookies = new Bun.CookieMap(headers.get("cookie") ?? "");
49
+ }
50
+ }
@@ -47,6 +47,7 @@ declare global {
47
47
  | undefined;
48
48
  var __AKAN_RSC_REFRESH__: ((options?: { buildId?: number }) => Promise<void>) | undefined;
49
49
  var __AKAN_RSC_CLEAR_CACHE__: (() => void) | undefined;
50
+ var __AKAN_RSC_IS_FROM_CACHE__: (() => boolean) | undefined;
50
51
  var __AKAN_DEV_SYNC_NAVIGATION__: ((href: string, kind: "push" | "replace" | "back" | "pop") => void) | undefined;
51
52
  var __AKAN_DEV_SYNC_NAVIGATION_APPLYING__: boolean | undefined;
52
53
  var __AKAN_GET_SYNC_ROUTE_HREF__: ((href: string) => string) | undefined;
@@ -299,8 +300,11 @@ let currentRouterState: AkanRouterStateV1 | null = initialRouterState;
299
300
  let currentSegmentTree: RscSegmentCacheNode | null = createAkanSegmentCacheTree(initialNode);
300
301
  let currentFullNode: RscCacheNode = initialNode;
301
302
  let currentCommitKind: "full" | "patch" = "full";
303
+ let currentCommitFromCache = false;
302
304
  let navigationSeq = 0;
303
305
 
306
+ globalThis.__AKAN_RSC_IS_FROM_CACHE__ = () => currentCommitFromCache;
307
+
304
308
  function rememberCommittedRouteState(node: RscCacheNode): void {
305
309
  rscPatchCache.clear();
306
310
  if (!node.routerState) return;
@@ -378,6 +382,7 @@ function Root(): ReactNode {
378
382
  maxEntries: MAX_RSC_CACHE_ENTRIES,
379
383
  startTransition,
380
384
  commitThenable: (node) => {
385
+ currentCommitFromCache = false;
381
386
  resetAkanSegmentOutletPatches();
382
387
  setThenable(node.thenable);
383
388
  },
@@ -397,6 +402,7 @@ function Root(): ReactNode {
397
402
  const scrollToTop = options.scrollToTop ?? true;
398
403
  try {
399
404
  let nextNode = rscCache.get(target);
405
+ const servedFromCache = !!nextNode;
400
406
  if (!nextNode) {
401
407
  const cachedPatch = rscPatchCache.get(target);
402
408
  if (cachedPatch) {
@@ -425,6 +431,7 @@ function Root(): ReactNode {
425
431
  bumpScrollToTop: () => setScrollToTopTick((tick) => tick + 1),
426
432
  })
427
433
  ) {
434
+ currentCommitFromCache = true;
428
435
  rememberPatchedRouteState(patchResult.tree, patchResult.patchedNode);
429
436
  rememberRscPatchCacheNode(rscPatchCache, cachedPatch, MAX_RSC_CACHE_ENTRIES);
430
437
  return;
@@ -449,6 +456,7 @@ function Root(): ReactNode {
449
456
  bumpScrollToTop: () => setScrollToTopTick((tick) => tick + 1),
450
457
  })
451
458
  ) {
459
+ currentCommitFromCache = false;
452
460
  rememberPatchedRouteState(fetched.tree, fetched.patchedNode);
453
461
  const patchCacheNode = createRscPatchNavigationCacheNode({
454
462
  href: target,
@@ -493,6 +501,7 @@ function Root(): ReactNode {
493
501
  maxEntries: MAX_RSC_CACHE_ENTRIES,
494
502
  startTransition,
495
503
  commitThenable: (node) => {
504
+ currentCommitFromCache = servedFromCache;
496
505
  resetAkanSegmentOutletPatches();
497
506
  setThenable(node.thenable);
498
507
  },