akanjs 3.0.1 → 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 +11 -0
- package/base/base.ts +1 -1
- package/client/cookie.ts +4 -6
- package/common/Logger.ts +10 -0
- 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/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/rscWorkerHost.ts +16 -7
- package/server/webRouter.ts +3 -0
- 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/common/Logger.d.ts +2 -0
- 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/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/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/buttonRecipe.d.ts +2 -2
- 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/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/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/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/webkit/RenderFailure.tsx +32 -0
- package/webkit/RenderLayer.tsx +22 -2
package/signal/middleware.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BackendEnv, Cls, PromiseOrObject } from "akanjs/base";
|
|
2
2
|
import { Logger } from "akanjs/common";
|
|
3
|
-
import { Exception } from "./exception";
|
|
3
|
+
import { Exception, isExceptionLike } from "./exception";
|
|
4
4
|
import type { SignalContext } from "./signalContext";
|
|
5
5
|
|
|
6
6
|
export interface Middleware<Env extends BackendEnv = BackendEnv> {
|
|
@@ -34,10 +34,11 @@ export class Logging extends middleware("logging") {
|
|
|
34
34
|
}
|
|
35
35
|
return result;
|
|
36
36
|
} catch (error) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const line = `Error ${context.endpointInfo.type}-${context.key} / ${Date.now() - start}ms: ${String(error)}`;
|
|
38
|
+
|
|
39
|
+
if (isExceptionLike(error) && error.statusCode === 429) {
|
|
40
|
+
if (debug) context.adaptor.logger.debug(line);
|
|
41
|
+
} else context.adaptor.logger.error(line);
|
|
41
42
|
throw error;
|
|
42
43
|
}
|
|
43
44
|
};
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
EndpointArgProps,
|
|
9
9
|
EndpointCls,
|
|
10
10
|
EndpointInfo,
|
|
11
|
+
ResolvedSliceCrud,
|
|
11
12
|
SerializedArg,
|
|
12
13
|
SerializedEndpoint,
|
|
13
14
|
SerializedFilter,
|
|
@@ -157,6 +158,7 @@ export class FetchSerializer {
|
|
|
157
158
|
sliceCls.removeGuards !== sliceCls.cruGuards,
|
|
158
159
|
),
|
|
159
160
|
...FetchSerializer.#serializeSliceMcp(sliceCls),
|
|
161
|
+
...FetchSerializer.#serializeSliceCrud(sliceCls),
|
|
160
162
|
...FetchSerializer.#serializeSliceAgents(sliceCls),
|
|
161
163
|
endpoint: FetchSerializer.#serializeEndpoints(endpointCls),
|
|
162
164
|
};
|
|
@@ -186,12 +188,21 @@ export class FetchSerializer {
|
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
static #serializeSliceMcp(sliceCls: SliceCls): { mcp?: SerializedSignalMcp } {
|
|
189
|
-
const mcp =
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
const mcp = FetchSerializer.#offVerbs(sliceCls.mcp);
|
|
192
|
+
return Object.keys(mcp).length ? { mcp } : {};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static #serializeSliceCrud(sliceCls: SliceCls): { crud?: SerializedSignalMcp } {
|
|
196
|
+
const crud = FetchSerializer.#offVerbs(sliceCls.crud);
|
|
197
|
+
return Object.keys(crud).length ? { crud } : {};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
static #offVerbs(verbs: ResolvedSliceCrud | undefined): SerializedSignalMcp {
|
|
201
|
+
return Object.fromEntries(
|
|
202
|
+
Object.entries(verbs ?? {})
|
|
203
|
+
.filter(([, on]) => !on)
|
|
192
204
|
.map(([verb]) => [verb, false]),
|
|
193
205
|
) as SerializedSignalMcp;
|
|
194
|
-
return Object.keys(mcp).length ? { mcp } : {};
|
|
195
206
|
}
|
|
196
207
|
|
|
197
208
|
static serializeServiceSignal(endpointCls: EndpointCls): SerializedSignal {
|
package/signal/signalContext.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Any,
|
|
3
3
|
type BackendEnv,
|
|
4
|
+
Binary,
|
|
4
5
|
type Cls,
|
|
5
6
|
FIELD_META,
|
|
6
7
|
INTERNAL_META,
|
|
@@ -21,8 +22,9 @@ import type { Internal, InternalCls, InternalInfo, MiddlewareCls } from ".";
|
|
|
21
22
|
import { CrossSiteGuard } from "./CrossSiteGuard";
|
|
22
23
|
import { EndpointCache } from "./endpointCache";
|
|
23
24
|
import type { ArgInfo, EndpointInfo, EndpointType } from "./endpointInfo";
|
|
25
|
+
import { EndpointRateLimit } from "./endpointRateLimit";
|
|
24
26
|
import { Exception, isExceptionLike } from "./exception";
|
|
25
|
-
import { type GuardCls, guardOf } from "./guard";
|
|
27
|
+
import { type GuardCls, type GuardScope, guardOf } from "./guard";
|
|
26
28
|
import { SignalFailure } from "./SignalFailure";
|
|
27
29
|
import { getCurrentTrace, runTraced, SignalTrace, type TraceOrigin, traceSpan } from "./trace";
|
|
28
30
|
|
|
@@ -46,6 +48,21 @@ type MiddlewareHandler = (context: SignalContext, next: () => Promise<unknown>)
|
|
|
46
48
|
/** Request-scoped: the outermost `resolveReturn` starts one and every recursion threads it down. */
|
|
47
49
|
type ResolveCache = Map<ConstantFieldTypeInput, Map<string, Promise<unknown>>>;
|
|
48
50
|
|
|
51
|
+
interface SignalContextOption<Ctx, Env extends BackendEnv> {
|
|
52
|
+
endpointInfo: EndpointInfo;
|
|
53
|
+
adaptor: Adaptor;
|
|
54
|
+
registry: InjectRegistry;
|
|
55
|
+
env: Env;
|
|
56
|
+
live: LiveRegistry;
|
|
57
|
+
middleware: Map<string, MiddlewareCls>;
|
|
58
|
+
/** A caller-built context: MCP's arguments arrive as one named object, not in a URL. */
|
|
59
|
+
ctx?: Ctx;
|
|
60
|
+
/** Defaults to the transport. */
|
|
61
|
+
origin?: TraceOrigin;
|
|
62
|
+
/** Unset, the context joins the current trace or opens one that `exec()` closes. */
|
|
63
|
+
trace?: SignalTrace | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
export class SignalContext<
|
|
50
67
|
Ctx extends HttpExecutionContext | WebSocketExecutionContext = HttpExecutionContext | WebSocketExecutionContext,
|
|
51
68
|
Env extends BackendEnv = BackendEnv,
|
|
@@ -57,40 +74,22 @@ export class SignalContext<
|
|
|
57
74
|
adaptor: Adaptor;
|
|
58
75
|
/** `mcp` for a call through the MCP endpoint. Held here, not on the trace: a null trace would let a guard fail open. */
|
|
59
76
|
readonly origin: TraceOrigin;
|
|
60
|
-
args: unknown[] = [];
|
|
61
77
|
internalArgs: unknown[] = [];
|
|
62
78
|
trace: SignalTrace | null = null;
|
|
79
|
+
readonly #request: Bun.BunRequest | WebSocketRequest;
|
|
80
|
+
readonly #option: SignalContextOption<Ctx, Env>;
|
|
81
|
+
#rawArgs: unknown[] | null = null;
|
|
82
|
+
#args: unknown[] | null = null;
|
|
63
83
|
#registry: InjectRegistry;
|
|
64
84
|
#env: Env;
|
|
65
85
|
#live: LiveRegistry;
|
|
66
86
|
#middleware: Map<string, MiddlewareCls>;
|
|
67
87
|
static #reported = new WeakSet<object>();
|
|
68
|
-
constructor(
|
|
69
|
-
|
|
70
|
-
reqOrWsReq: Bun.BunRequest | WebSocketRequest,
|
|
71
|
-
{
|
|
72
|
-
endpointInfo,
|
|
73
|
-
adaptor,
|
|
74
|
-
registry,
|
|
75
|
-
env,
|
|
76
|
-
live,
|
|
77
|
-
middleware,
|
|
78
|
-
ctx,
|
|
79
|
-
origin,
|
|
80
|
-
}: {
|
|
81
|
-
endpointInfo: EndpointInfo;
|
|
82
|
-
adaptor: Adaptor;
|
|
83
|
-
registry: InjectRegistry;
|
|
84
|
-
env: Env;
|
|
85
|
-
live: LiveRegistry;
|
|
86
|
-
middleware: Map<string, MiddlewareCls>;
|
|
87
|
-
/** A caller-built context: MCP's arguments arrive as one named object, not in a URL. */
|
|
88
|
-
ctx?: Ctx;
|
|
89
|
-
/** Defaults to the transport. */
|
|
90
|
-
origin?: TraceOrigin;
|
|
91
|
-
},
|
|
92
|
-
) {
|
|
88
|
+
constructor(key: string, reqOrWsReq: Bun.BunRequest | WebSocketRequest, option: SignalContextOption<Ctx, Env>) {
|
|
89
|
+
const { endpointInfo, adaptor, registry, env, live, middleware, ctx, origin, trace } = option;
|
|
93
90
|
this.key = key;
|
|
91
|
+
this.#request = reqOrWsReq;
|
|
92
|
+
this.#option = option;
|
|
94
93
|
this.transport = httpEndpointTypes.has(endpointInfo.type) ? "http" : "websocket";
|
|
95
94
|
this.origin = origin ?? this.transport;
|
|
96
95
|
this.endpointInfo = endpointInfo;
|
|
@@ -103,7 +102,10 @@ export class SignalContext<
|
|
|
103
102
|
this.#live = live;
|
|
104
103
|
this.#middleware = middleware;
|
|
105
104
|
|
|
106
|
-
this.trace =
|
|
105
|
+
this.trace =
|
|
106
|
+
trace !== undefined
|
|
107
|
+
? trace
|
|
108
|
+
: (getCurrentTrace() ?? SignalTrace.create(key, endpointInfo.type, origin ?? this.transport));
|
|
107
109
|
if (this.trace && this.transport === "http")
|
|
108
110
|
this.trace.applyDebugHeader((reqOrWsReq as { headers?: Headers }).headers?.get?.("x-akan-debug"));
|
|
109
111
|
}
|
|
@@ -120,28 +122,61 @@ export class SignalContext<
|
|
|
120
122
|
if (!service) throw new Exception.Error(`Service "${refName}" not found in live registry`);
|
|
121
123
|
return service as T;
|
|
122
124
|
}
|
|
125
|
+
/** Reads the request but parses no argument: that waits for the account guards in `exec()`. */
|
|
123
126
|
async init() {
|
|
124
127
|
|
|
125
128
|
if (this.endpointInfo.type === "mutation" && this.transport === "http") {
|
|
126
129
|
const httpCtx = this.getHttpContext();
|
|
127
130
|
CrossSiteGuard.assertOrigin(httpCtx.req, httpCtx.url, this.key);
|
|
128
131
|
}
|
|
132
|
+
|
|
133
|
+
if (!this.#isLeavingRoom()) await EndpointRateLimit.admit(this, "ip");
|
|
129
134
|
const start = this.trace ? performance.now() : 0;
|
|
130
|
-
this
|
|
131
|
-
this.trace?.recordSpan("
|
|
135
|
+
this.#rawArgs = await this.ctx.readArgs(this.endpointInfo);
|
|
136
|
+
this.trace?.recordSpan("argRead", performance.now() - start);
|
|
132
137
|
return this;
|
|
133
138
|
}
|
|
139
|
+
/** Parsed on first read, so a guard or middleware that reads one still gets it; a parser runs at most once per call. */
|
|
140
|
+
get args(): unknown[] {
|
|
141
|
+
this.#args ??= this.#parseArgs();
|
|
142
|
+
return this.#args;
|
|
143
|
+
}
|
|
144
|
+
set args(args: unknown[]) {
|
|
145
|
+
this.#args = args;
|
|
146
|
+
}
|
|
147
|
+
#parseArgs(): unknown[] {
|
|
148
|
+
const raw = this.#rawArgs;
|
|
149
|
+
if (!raw) return [];
|
|
150
|
+
const start = this.trace ? performance.now() : 0;
|
|
151
|
+
const args = this.endpointInfo.args.map((arg, idx) => this.ctx.parseArg(arg, raw[idx]));
|
|
152
|
+
this.trace?.recordSpan("argParse", performance.now() - start);
|
|
153
|
+
return args;
|
|
154
|
+
}
|
|
155
|
+
#isLeavingRoom() {
|
|
156
|
+
return this.transport === "websocket" && this.getWebSocketContext().eventType === "unsubscribe";
|
|
157
|
+
}
|
|
134
158
|
|
|
135
|
-
async #checkGuards() {
|
|
159
|
+
async #checkGuards(scope?: GuardScope) {
|
|
136
160
|
for (const GuardCls of this.endpointInfo.signalOption.guards ?? []) {
|
|
161
|
+
if (scope && (GuardCls.scope === "account" ? "account" : "resource") !== scope) continue;
|
|
137
162
|
if (!(await guardOf(GuardCls).canPass(this)))
|
|
138
163
|
throw new Exception.Forbidden(`Access denied by guard: ${GuardCls.name}`);
|
|
139
164
|
}
|
|
140
165
|
}
|
|
141
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* Re-checks the guards of a subscribed room outside a request; only global middlewares (account resolution) run. On
|
|
168
|
+
* a fresh context, so a guard that memoizes per context cannot replay the verdict it gave at subscribe.
|
|
169
|
+
*/
|
|
142
170
|
async authorize(): Promise<boolean> {
|
|
171
|
+
const fresh = new SignalContext<Ctx, Env>(this.key, this.#request, {
|
|
172
|
+
...this.#option,
|
|
173
|
+
ctx: undefined,
|
|
174
|
+
trace: null,
|
|
175
|
+
});
|
|
176
|
+
fresh.#rawArgs = this.#rawArgs;
|
|
177
|
+
fresh.#args = this.#args;
|
|
143
178
|
try {
|
|
144
|
-
await
|
|
179
|
+
await fresh.#withMiddleware(async () => await fresh.#checkGuards(), { endpointMiddlewares: false })();
|
|
145
180
|
return true;
|
|
146
181
|
} catch {
|
|
147
182
|
return false;
|
|
@@ -239,11 +274,16 @@ export class SignalContext<
|
|
|
239
274
|
if (!this.endpointInfo.execFn) throw new Exception.Error("Exec function is not set");
|
|
240
275
|
const coreExec = async () => {
|
|
241
276
|
if (!this.endpointInfo.execFn) throw new Exception.Error("Exec function is not set");
|
|
277
|
+
await EndpointRateLimit.admit(this, "account");
|
|
242
278
|
if (this.trace) {
|
|
243
|
-
await traceSpan("guards", () => this.#checkGuards());
|
|
279
|
+
await traceSpan("guards", () => this.#checkGuards("account"));
|
|
244
280
|
const account = this.get<{ id?: unknown }>("account");
|
|
245
281
|
if (typeof account?.id === "string") this.trace.setAttr("userId", account.id);
|
|
246
|
-
} else await this.#checkGuards();
|
|
282
|
+
} else await this.#checkGuards("account");
|
|
283
|
+
|
|
284
|
+
this.#args ??= this.#parseArgs();
|
|
285
|
+
if (this.trace) await traceSpan("resourceGuards", () => this.#checkGuards("resource"));
|
|
286
|
+
else await this.#checkGuards("resource");
|
|
247
287
|
if (this.endpointInfo.internalArgs.length > 0) {
|
|
248
288
|
this.internalArgs = await Promise.all(
|
|
249
289
|
this.endpointInfo.internalArgs.map((arg) => {
|
|
@@ -271,11 +311,21 @@ export class SignalContext<
|
|
|
271
311
|
};
|
|
272
312
|
if (!this.trace) {
|
|
273
313
|
const resolved = await SignalContext.resolveReturn(result, resolveOption);
|
|
274
|
-
return this.ctx.makeResponse(this.#
|
|
314
|
+
return this.ctx.makeResponse(this.#settle(resolved), this.endpointInfo);
|
|
275
315
|
}
|
|
276
316
|
const resolved = await traceSpan("resolveReturn", () => SignalContext.resolveReturn(result, resolveOption));
|
|
277
|
-
return await traceSpan("serialize", async () =>
|
|
278
|
-
|
|
317
|
+
return await traceSpan("serialize", async () => this.ctx.makeResponse(this.#settle(resolved), this.endpointInfo));
|
|
318
|
+
}
|
|
319
|
+
#settle(resolved: unknown) {
|
|
320
|
+
this.#refuseBytesThroughAny(resolved);
|
|
321
|
+
return this.#settleUndefined(resolved);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#refuseBytesThroughAny(resolved: unknown) {
|
|
325
|
+
if (this.endpointInfo.returns.returnRef !== Any) return;
|
|
326
|
+
if (!(resolved instanceof ArrayBuffer) && !ArrayBuffer.isView(resolved)) return;
|
|
327
|
+
throw new Error(
|
|
328
|
+
`${this.endpointInfo.type} ${this.key} returns bytes through Any, which JSON cannot carry: declare Binary as its return`,
|
|
279
329
|
);
|
|
280
330
|
}
|
|
281
331
|
|
|
@@ -332,12 +382,17 @@ export class SignalContext<
|
|
|
332
382
|
path: endpointInfo.getPath(key),
|
|
333
383
|
timestamp: new Date().toISOString(),
|
|
334
384
|
}),
|
|
335
|
-
{ status: error.statusCode, headers:
|
|
385
|
+
{ status: error.statusCode, headers: SignalContext.#exceptionHeaders(error) },
|
|
336
386
|
);
|
|
337
387
|
}
|
|
338
388
|
return SignalFailure.response(error, { path: endpointInfo.getPath(key) });
|
|
339
389
|
}
|
|
340
390
|
}
|
|
391
|
+
static #exceptionHeaders(error: { statusCode: number }): Record<string, string> {
|
|
392
|
+
const seconds = (error as { data?: { seconds?: unknown } }).data?.seconds;
|
|
393
|
+
if (error.statusCode !== 429 || typeof seconds !== "number") return { "Content-Type": "application/json" };
|
|
394
|
+
return { "Content-Type": "application/json", "Retry-After": String(Math.max(1, Math.ceil(seconds))) };
|
|
395
|
+
}
|
|
341
396
|
static async resolveReturn(
|
|
342
397
|
value: unknown,
|
|
343
398
|
{
|
|
@@ -573,16 +628,24 @@ export class SignalContext<
|
|
|
573
628
|
* `::ffff:`; `null`, never a placeholder, when neither is known.
|
|
574
629
|
*/
|
|
575
630
|
getClientIp(): string | null {
|
|
576
|
-
if (this.transport === "http")
|
|
577
|
-
const { req } = this.getHttpContext();
|
|
578
|
-
|
|
579
|
-
const peer = SignalContext.#httpPeer?.(req);
|
|
580
|
-
return TrustedProxy.clientAddress(req.headers, peer === undefined ? undefined : (peer?.address ?? null));
|
|
581
|
-
}
|
|
631
|
+
if (this.transport === "http") return SignalContext.clientIpOf(this.getHttpContext().req);
|
|
582
632
|
const { ws } = this.getWebSocketContext<{ headers?: Headers }>();
|
|
583
633
|
if (!ws.data.headers) return ws.remoteAddress ? normalizeIpAddress(ws.remoteAddress) : null;
|
|
584
634
|
return TrustedProxy.clientAddress(ws.data.headers, ws.remoteAddress);
|
|
585
635
|
}
|
|
636
|
+
static #carriedIps = new WeakMap<Request, string | null>();
|
|
637
|
+
/** A request a proxy rebuilt has no socket behind it for `requestIP`, so it keeps the address its original resolved. */
|
|
638
|
+
static carryClientIp(target: Request, source: Request) {
|
|
639
|
+
if (target !== source) SignalContext.#carriedIps.set(target, SignalContext.clientIpOf(source));
|
|
640
|
+
}
|
|
641
|
+
/** `getClientIp` for a request no context wraps, as the MCP router and the page renderer hold one. */
|
|
642
|
+
static clientIpOf(req: Request): string | null {
|
|
643
|
+
const carried = SignalContext.#carriedIps.get(req);
|
|
644
|
+
if (carried !== undefined) return carried;
|
|
645
|
+
|
|
646
|
+
const peer = SignalContext.#httpPeer?.(req);
|
|
647
|
+
return TrustedProxy.clientAddress(req.headers, peer === undefined ? undefined : (peer?.address ?? null));
|
|
648
|
+
}
|
|
586
649
|
/** The caller's source port as the nearest proxy recorded it, else this socket's own. */
|
|
587
650
|
getClientPort(): number | null {
|
|
588
651
|
if (this.transport === "http") {
|
|
@@ -668,6 +731,11 @@ export class HttpExecutionContext<Appended = unknown> {
|
|
|
668
731
|
}
|
|
669
732
|
}
|
|
670
733
|
async getArgs(endpointInfo: EndpointInfo): Promise<unknown[]> {
|
|
734
|
+
const raw = await this.readArgs(endpointInfo);
|
|
735
|
+
return endpointInfo.args.map((arg, idx) => this.parseArg(arg, raw[idx]));
|
|
736
|
+
}
|
|
737
|
+
/** Each argument as the request carries it; `parseArg` turns one into its declared type. */
|
|
738
|
+
async readArgs(endpointInfo: EndpointInfo): Promise<unknown[]> {
|
|
671
739
|
if (endpointInfo.args.length === 0) return [];
|
|
672
740
|
this.params = this.req.params;
|
|
673
741
|
const hasBodyArgs = endpointInfo.args.some((arg) => arg.type === "body");
|
|
@@ -694,29 +762,42 @@ export class HttpExecutionContext<Appended = unknown> {
|
|
|
694
762
|
}
|
|
695
763
|
}
|
|
696
764
|
|
|
697
|
-
|
|
765
|
+
return endpointInfo.args.map((arg) => {
|
|
698
766
|
switch (arg.type) {
|
|
699
767
|
case "param":
|
|
700
|
-
return
|
|
768
|
+
return this.params[arg.name];
|
|
701
769
|
case "body":
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const raw = arg.arrDepth ? this.url.searchParams.getAll(arg.name) : this.url.searchParams.get(arg.name);
|
|
706
|
-
const value = arg.argRef === Any ? HttpExecutionContext.#parseAny(arg.name, raw) : raw;
|
|
707
|
-
const result = deserializeArg(arg, value);
|
|
708
|
-
this.searchParams[arg.name] = result;
|
|
709
|
-
return result;
|
|
710
|
-
}
|
|
770
|
+
return this.body[arg.name];
|
|
771
|
+
case "search":
|
|
772
|
+
return arg.arrDepth ? this.url.searchParams.getAll(arg.name) : this.url.searchParams.get(arg.name);
|
|
711
773
|
default:
|
|
712
774
|
return undefined;
|
|
713
775
|
}
|
|
714
776
|
});
|
|
715
|
-
|
|
777
|
+
}
|
|
778
|
+
parseArg(arg: ArgInfo, raw: unknown): unknown {
|
|
779
|
+
switch (arg.type) {
|
|
780
|
+
case "param":
|
|
781
|
+
return deserializeArg(arg, raw);
|
|
782
|
+
case "body":
|
|
783
|
+
return arg.argRef === Upload ? raw : deserializeArg(arg, raw);
|
|
784
|
+
case "search": {
|
|
785
|
+
const value =
|
|
786
|
+
arg.argRef === Any ? HttpExecutionContext.#parseAny(arg.name, raw as string | string[] | null) : raw;
|
|
787
|
+
const result = deserializeArg(arg, value);
|
|
788
|
+
this.searchParams[arg.name] = result;
|
|
789
|
+
return result;
|
|
790
|
+
}
|
|
791
|
+
default:
|
|
792
|
+
return undefined;
|
|
793
|
+
}
|
|
716
794
|
}
|
|
717
795
|
makeResponse(result: unknown, endpointInfo: EndpointInfo) {
|
|
718
796
|
if (result instanceof Response) return result;
|
|
719
797
|
if (endpointInfo.returns.arrDepth === 0 && PrimitiveRegistry.has(endpointInfo.returns.returnRef as Cls)) {
|
|
798
|
+
|
|
799
|
+
if (endpointInfo.returns.returnRef === Binary && result instanceof Uint8Array)
|
|
800
|
+
return this.res.json(Binary.serializeValue(result));
|
|
720
801
|
return this.res.json(result);
|
|
721
802
|
}
|
|
722
803
|
const value = serialize(endpointInfo.returns.returnRef, endpointInfo.returns.arrDepth, result, "object", {
|
|
@@ -740,16 +821,16 @@ export class WebSocketExecutionContext<Appended = unknown> {
|
|
|
740
821
|
this.eventType = wsReq.eventType;
|
|
741
822
|
}
|
|
742
823
|
async getArgs(endpointInfo: EndpointInfo): Promise<unknown[]> {
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
return
|
|
824
|
+
const raw = await this.readArgs(endpointInfo);
|
|
825
|
+
return endpointInfo.args.map((arg, idx) => this.parseArg(arg, raw[idx]));
|
|
826
|
+
}
|
|
827
|
+
async readArgs(endpointInfo: EndpointInfo): Promise<unknown[]> {
|
|
828
|
+
return endpointInfo.args.map((arg, idx) =>
|
|
829
|
+
arg.type === "msg" || arg.type === "room" ? this.data[idx] : undefined,
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
parseArg(arg: ArgInfo, raw: unknown): unknown {
|
|
833
|
+
return arg.type === "msg" || arg.type === "room" ? deserializeArg(arg, raw) : undefined;
|
|
753
834
|
}
|
|
754
835
|
makeResponse(result: unknown, endpointInfo: EndpointInfo) {
|
|
755
836
|
return serialize(endpointInfo.returns.returnRef, endpointInfo.returns.arrDepth, result, "object", {
|
package/signal/slice.ts
CHANGED
|
@@ -39,6 +39,7 @@ export type SliceCls<
|
|
|
39
39
|
updateGuards: GuardCls[];
|
|
40
40
|
removeGuards: GuardCls[];
|
|
41
41
|
mcp: ResolvedSliceMcp;
|
|
42
|
+
crud: ResolvedSliceCrud;
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
/** `root` is absent: it rides on the root slice itself. */
|
|
@@ -49,14 +50,26 @@ export interface ResolvedSliceMcp {
|
|
|
49
50
|
remove: boolean;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/** Which generated verbs the server mounts: a verb whose guards resolve to none (or `false`) has no route at all. */
|
|
54
|
+
export interface ResolvedSliceCrud {
|
|
55
|
+
get: boolean;
|
|
56
|
+
create: boolean;
|
|
57
|
+
update: boolean;
|
|
58
|
+
remove: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
interface RootSliceOption {
|
|
62
|
+
/**
|
|
63
|
+
* `false` (or no guard at all) mounts no generated endpoint for that verb, which is the only way to take one off
|
|
64
|
+
* HTTP: `mcp: { cru: false }` merely keeps it off the agent shelf. `create`/`update`/`remove` fall back to `cru`.
|
|
65
|
+
*/
|
|
53
66
|
guards?: {
|
|
54
67
|
root?: GuardCls | GuardCls[];
|
|
55
|
-
get?: GuardCls | GuardCls[];
|
|
56
|
-
cru?: GuardCls | GuardCls[];
|
|
57
|
-
create?: GuardCls | GuardCls[];
|
|
58
|
-
update?: GuardCls | GuardCls[];
|
|
59
|
-
remove?: GuardCls | GuardCls[];
|
|
68
|
+
get?: GuardCls | GuardCls[] | false;
|
|
69
|
+
cru?: GuardCls | GuardCls[] | false;
|
|
70
|
+
create?: GuardCls | GuardCls[] | false;
|
|
71
|
+
update?: GuardCls | GuardCls[] | false;
|
|
72
|
+
remove?: GuardCls | GuardCls[] | false;
|
|
60
73
|
};
|
|
61
74
|
/**
|
|
62
75
|
* Keyed like `guards`, for the root slice and generated CRUD only; `create`/`update`/`remove` fall back to `cru`
|
|
@@ -148,14 +161,21 @@ export function slice<
|
|
|
148
161
|
if (!srv.cnst || !srv.db) throw new Error("cnst and db are required");
|
|
149
162
|
const filterRef = srv.db.filter;
|
|
150
163
|
const init = buildSlice(srv.srv.refName, srv.cnst.input, srv.cnst.full, srv.cnst.light, srv.cnst.insight, filterRef);
|
|
151
|
-
const toGuards = (guard?: GuardCls | GuardCls[]) => (guard ? (Array.isArray(guard) ? guard : [guard]) : []);
|
|
164
|
+
const toGuards = (guard?: GuardCls | GuardCls[] | false) => (guard ? (Array.isArray(guard) ? guard : [guard]) : []);
|
|
152
165
|
const rootGuards = toGuards(option.guards?.root);
|
|
153
166
|
const getGuards = toGuards(option.guards?.get);
|
|
154
167
|
const cruGuards = toGuards(option.guards?.cru);
|
|
155
168
|
|
|
156
|
-
const createGuards = option.guards?.create ? toGuards(option.guards.create) : cruGuards;
|
|
157
|
-
const updateGuards = option.guards?.update ? toGuards(option.guards.update) : cruGuards;
|
|
158
|
-
const removeGuards = option.guards?.remove ? toGuards(option.guards.remove) : cruGuards;
|
|
169
|
+
const createGuards = option.guards?.create !== undefined ? toGuards(option.guards.create) : cruGuards;
|
|
170
|
+
const updateGuards = option.guards?.update !== undefined ? toGuards(option.guards.update) : cruGuards;
|
|
171
|
+
const removeGuards = option.guards?.remove !== undefined ? toGuards(option.guards.remove) : cruGuards;
|
|
172
|
+
|
|
173
|
+
const crud: ResolvedSliceCrud = {
|
|
174
|
+
get: getGuards.length > 0,
|
|
175
|
+
create: createGuards.length > 0,
|
|
176
|
+
update: updateGuards.length > 0,
|
|
177
|
+
remove: removeGuards.length > 0,
|
|
178
|
+
};
|
|
159
179
|
const mcpOption =
|
|
160
180
|
typeof option.mcp === "boolean" ? { root: option.mcp, get: option.mcp, cru: option.mcp } : option.mcp;
|
|
161
181
|
const cruMcp = mcpOption?.cru !== false;
|
|
@@ -179,6 +199,7 @@ export function slice<
|
|
|
179
199
|
static updateGuards = updateGuards;
|
|
180
200
|
static removeGuards = removeGuards;
|
|
181
201
|
static mcp = mcp;
|
|
202
|
+
static crud = crud;
|
|
182
203
|
|
|
183
204
|
static [SLICE_META] = Object.assign(
|
|
184
205
|
{},
|
package/signal/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DefaultPrimitiveName, UnCls } from "akanjs/base";
|
|
2
2
|
import type { ServiceModel } from "akanjs/service";
|
|
3
|
+
import type { RateLimitBudget } from "./endpointRateLimit";
|
|
3
4
|
import type { GuardCls } from "./guard";
|
|
4
5
|
import type { MiddlewareCls } from "./middleware";
|
|
5
6
|
import type { SliceCls } from "./slice";
|
|
@@ -94,6 +95,11 @@ export interface SignalOption<Response = any, Nullable extends boolean = false,
|
|
|
94
95
|
* internal argument may carry one (those answer per caller), and the lookup runs after the guards.
|
|
95
96
|
*/
|
|
96
97
|
cache?: number;
|
|
98
|
+
/**
|
|
99
|
+
* Calls per caller per window, refused with 429 `base.error.tooManyRequests`. `by: "ip"` is counted before the body
|
|
100
|
+
* is read, `by: "account"` after the middlewares and before the guards; `false` exempts it from the app's default.
|
|
101
|
+
*/
|
|
102
|
+
rateLimit?: RateLimitBudget | false;
|
|
97
103
|
guards?: GuardCls[];
|
|
98
104
|
middlewares?: MiddlewareCls[];
|
|
99
105
|
prefix?: false | string;
|
|
@@ -181,6 +187,8 @@ export interface SerializedSignal {
|
|
|
181
187
|
removeGuards?: string[];
|
|
182
188
|
/** Generated CRUD verbs kept off the agent shelf, carried here because `FetchClient.getBaseEndpoint` synthesizes them. */
|
|
183
189
|
mcp?: SerializedSignalMcp;
|
|
190
|
+
/** Generated CRUD verbs the server does not mount at all, so no client synthesizes them. */
|
|
191
|
+
crud?: SerializedSignalMcp;
|
|
184
192
|
/** Which generated CRUD verbs a person-only guard protects, by the same verb map; only the `false` keys travel. */
|
|
185
193
|
agents?: SerializedSignalMcp;
|
|
186
194
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BackendEnv, BaseEnv } from "akanjs/base";
|
|
2
|
-
import type
|
|
2
|
+
import { FetchClient, type FetchProxy } from "akanjs/fetch";
|
|
3
3
|
import type { AkanLib } from "akanjs/server";
|
|
4
4
|
import { type TestEnv, TestServer, type TestServerOptions } from "./testServer";
|
|
5
5
|
|
|
@@ -113,6 +113,7 @@ export const setupSignalTestTarget = async <Fetch = FetchProxy>(
|
|
|
113
113
|
storage: "memory",
|
|
114
114
|
...resolvedOptions,
|
|
115
115
|
});
|
|
116
|
+
FetchClient.useTestClientIps();
|
|
116
117
|
await testServer.init();
|
|
117
118
|
currentContext = {
|
|
118
119
|
fetch: target.fetch,
|
package/types/base/base.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type EnumInstance<RefName extends string = string, T = string | number> =
|
|
|
23
23
|
/** True for a `class X extends enumOf(...) {}`, not for the bare `enumOf(...)` result. */
|
|
24
24
|
export declare const isEnum: (enumRef: Cls) => boolean;
|
|
25
25
|
/** `type` is `String` for string values, else `Int` when every value is an integer, else `Float`. */
|
|
26
|
-
export declare const enumOf: <RefName extends string, T = string | number>(refName: RefName, values: readonly T[]) => EnumInstance<RefName, T>;
|
|
26
|
+
export declare const enumOf: <RefName extends string, const T = string | number>(refName: RefName, values: readonly T[]) => EnumInstance<RefName, T>;
|
|
27
27
|
export declare class DataList<Light extends {
|
|
28
28
|
id: string;
|
|
29
29
|
}> {
|
package/types/client/cookie.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare const cookies: () => Map<string, {
|
|
|
9
9
|
name: string;
|
|
10
10
|
value: string;
|
|
11
11
|
}>;
|
|
12
|
-
export declare const setCookie: (key: string, value: string,
|
|
12
|
+
export declare const setCookie: (key: string, value: string, { path, sameSite, secure }?: CookieOptions) => void;
|
|
13
13
|
/** Reads through `cookies()` on both sides: a hand-rolled `split("=")` cuts base64 padding and skips the `j:` form. */
|
|
14
14
|
export declare const getCookie: (key: string) => string | undefined;
|
|
15
15
|
export declare const removeCookie: (key: string, options?: {
|
package/types/common/Logger.d.ts
CHANGED
|
@@ -77,6 +77,8 @@ export declare class Logger {
|
|
|
77
77
|
static formatAttrs(attrs: LogAttrs): string;
|
|
78
78
|
/** A flight-recorder or `x-akan-debug` record passes every floor between here and the reader. */
|
|
79
79
|
static isPromoted(record: LogRecord): boolean;
|
|
80
|
+
/** `value` as one line of JSON for a message, a secret-named key masked at any depth and the text cut at `max`. */
|
|
81
|
+
static inlineData(value: unknown, max?: number): string;
|
|
80
82
|
static redactAttrs(attrs: LogAttrs): LogAttrs;
|
|
81
83
|
static emit({ level, name, message, context, attrs }: LoggerEmitInput): void;
|
|
82
84
|
/** Writes a failed request's flight-recorder records past the console level, each marked `flight`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const baseDictionary: import("./dictInfo.d.ts").ServiceDictInfo<[string, string], "ping" | "pingBody" | "pingParam" | "pingQuery" | "wsPing" | "pubsubPing", "serverUnreachable" | "serverUnavailable" | "gatewayTimeout" | "unexpectedResponse", "edit" | "error" | "ok" | "approve" | "remove" | "unauthorized" | "save" | "refresh" | "password" | "agent" | "skip" | "stop" | "send" | "success" | "somethingWrong" | "connecting" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentToolCard" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachReading" | "agentReferenceRemove" | "agentReferenceFailed" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentQueue" | "agentQueuePlaceholder" | "agentQueued" | "agentQueueEdit" | "agentQueueCancel" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentTokensOf" | "agentCompactsAt" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "noSelection" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "draftConflict" | "draftApplied" | "draftRestore" | "draftDiscard" | "draftStartOver" | "new" | "view">;
|
|
1
|
+
export declare const baseDictionary: import("./dictInfo.d.ts").ServiceDictInfo<[string, string], "ping" | "pingBody" | "pingParam" | "pingQuery" | "wsPing" | "pubsubPing", "serverUnreachable" | "serverUnavailable" | "gatewayTimeout" | "tooManyRequests" | "unexpectedResponse" | "immutableField", "edit" | "error" | "ok" | "retry" | "approve" | "remove" | "unauthorized" | "save" | "refresh" | "password" | "agent" | "skip" | "stop" | "send" | "success" | "somethingWrong" | "connecting" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "pageLoadFailed" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentToolCard" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachReading" | "agentReferenceRemove" | "agentReferenceFailed" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentQueue" | "agentQueuePlaceholder" | "agentQueued" | "agentQueueEdit" | "agentQueueCancel" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentTokensOf" | "agentCompactsAt" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "noSelection" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "draftConflict" | "draftApplied" | "draftRestore" | "draftDiscard" | "draftStartOver" | "new" | "view">;
|