@frockbot/cloudflare 0.0.0 → 0.7.291
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/build-artifact.ts +130 -0
- package/build-flutter-web.ts +508 -0
- package/deployment-config/README.md +414 -0
- package/deployment-config/cli.ts +228 -0
- package/deployment-config/generate.ts +689 -0
- package/deployment-config/jsonc.ts +18 -0
- package/deployment-config/profile-schema.generated.ts +391 -0
- package/deployment-config/profile.schema.json +358 -0
- package/deployment-config/profile.ts +124 -0
- package/migrations/0001_better_auth.sql +15 -0
- package/migrations/0002_drop_account_issuer.sql +8 -0
- package/package.json +61 -5
- package/release-version.ts +20 -0
- package/src/account-admission.ts +179 -0
- package/src/account-deletion.ts +274 -0
- package/src/admin-entrypoint.ts +194 -0
- package/src/admin-identities.ts +26 -0
- package/src/audit.ts +103 -0
- package/src/auth-package.access.ts +24 -0
- package/src/auth-package.ts +36 -0
- package/src/avatar-state-cleanup.ts +107 -0
- package/src/billing-computer.ts +267 -0
- package/src/billing-readiness.ts +21 -0
- package/src/billing.ts +461 -0
- package/src/bot-capabilities.ts +480 -0
- package/src/bot-recovery.ts +1 -0
- package/src/bot-state-channel.ts +702 -0
- package/src/bot-state.ts +3995 -0
- package/src/bot-template-cleanup.ts +98 -0
- package/src/bot-title-cleanup.ts +104 -0
- package/src/brand-icon.png +0 -0
- package/src/brand-logo.ts +2 -0
- package/src/brand.ts +36 -0
- package/src/client-compatibility.ts +51 -0
- package/src/compaction-announcement-cleanup.ts +113 -0
- package/src/computer-egress.ts +110 -0
- package/src/computer-host.ts +92 -0
- package/src/computer-screenshot-cleanup.ts +78 -0
- package/src/contracts.ts +832 -0
- package/src/debug.ts +272 -0
- package/src/default-packages-marker-cleanup.ts +39 -0
- package/src/deployment-policy-admin-host.ts +121 -0
- package/src/deployment-policy.ts +426 -0
- package/src/directory-profile-cleanup.ts +101 -0
- package/src/durable-rpc.ts +753 -0
- package/src/durable-session.ts +21 -0
- package/src/entry-boundary.ts +103 -0
- package/src/frock-ai.ts +338 -0
- package/src/gateway.ts +1392 -0
- package/src/group-chat.ts +736 -0
- package/src/hidden-bot-notifications-cleanup.ts +32 -0
- package/src/index.ts +2798 -0
- package/src/insights.ts +15 -0
- package/src/machine-messages-cleanup.ts +121 -0
- package/src/machine-socket.ts +164 -0
- package/src/memory-records.ts +181 -0
- package/src/memory.ts +145 -0
- package/src/model-rates.ts +252 -0
- package/src/native-auth.ts +1006 -0
- package/src/native-sessions.ts +121 -0
- package/src/notification-state-cleanup.ts +18 -0
- package/src/ollama-web-search-cleanup.ts +69 -0
- package/src/package-page-shapes-cleanup.ts +190 -0
- package/src/plugin-egress.ts +31 -0
- package/src/plugin-page-route.ts +72 -0
- package/src/plugin-panels-cleanup.ts +101 -0
- package/src/prepared-input-cleanup.ts +105 -0
- package/src/production-secrets.ts +484 -0
- package/src/project-cleanup.ts +123 -0
- package/src/project-events-cleanup.ts +152 -0
- package/src/publication-state-cleanup.ts +92 -0
- package/src/push.ts +498 -0
- package/src/request-body.ts +165 -0
- package/src/routine-state-cleanup.ts +57 -0
- package/src/search.ts +88 -0
- package/src/sidebar-label-cleanup.ts +165 -0
- package/src/skill-index-cleanup.ts +53 -0
- package/src/supersede-cleanup.ts +162 -0
- package/src/test-chat-cleanup.ts +93 -0
- package/src/uploads.ts +486 -0
- package/src/user-application.ts +1068 -0
- package/src/user-configuration.ts +4491 -0
- package/src/voice-assistant.ts +4999 -0
- package/src/voice-dictation.ts +743 -0
- package/src/working-context-cleanup.ts +96 -0
- package/src/workspace.ts +201 -0
- package/wrangler.jsonc +385 -0
- package/README.md +0 -3
package/src/debug.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeBotDebugQueryV1,
|
|
3
|
+
isBotDebugQueryRefusalV1,
|
|
4
|
+
type BotDebugQueryV1,
|
|
5
|
+
} from "@frockbot/app/shell/debug-protocol";
|
|
6
|
+
import {
|
|
7
|
+
decodeSetUserFeaturesCommandV1,
|
|
8
|
+
type SetUserFeaturesCommandV1,
|
|
9
|
+
type UserFeaturesV1,
|
|
10
|
+
} from "@frockbot/app/admin/shared";
|
|
11
|
+
import { decodeBotIdV1 } from "@frockbot/core/configuration";
|
|
12
|
+
import { constantTimeEqualsV1 } from "@frockbot/core/crypto";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The operator surface: `/api/debug/*`, authorized by a shared token rather
|
|
16
|
+
* than by a session, so it can be read from a terminal while a Bot is wedged
|
|
17
|
+
* and nobody is signed in.
|
|
18
|
+
*
|
|
19
|
+
* Two routes write. A Turn send requires both the debug token and an
|
|
20
|
+
* owner/admin identity, then enters the ordinary client Turn path with its
|
|
21
|
+
* ownership check, admission/refusal handling and size limit. An account's
|
|
22
|
+
* features write is the operator's half of administration: the admin portal is
|
|
23
|
+
* the hosted deployment's surface for it (ADR 0028), and a deployment with no
|
|
24
|
+
* portal — a self-hosted one, a test stack — turns Plugin authoring on for an
|
|
25
|
+
* account from here, under the same deployment secret. Nothing here
|
|
26
|
+
* can reconcile, cancel, or otherwise move an existing run.
|
|
27
|
+
*/
|
|
28
|
+
export interface DebugGatewaySurface {
|
|
29
|
+
/** Absent (or empty) disables the whole surface; the routes then 404. */
|
|
30
|
+
token?: string;
|
|
31
|
+
listUsers(): Promise<
|
|
32
|
+
Array<{ id: string; email: string; name: string; createdAt: string }>
|
|
33
|
+
>;
|
|
34
|
+
listBots(userId: string): Promise<unknown>;
|
|
35
|
+
snapshot(
|
|
36
|
+
userId: string,
|
|
37
|
+
botId: string,
|
|
38
|
+
query: BotDebugQueryV1,
|
|
39
|
+
): Promise<unknown>;
|
|
40
|
+
/**
|
|
41
|
+
* The User's voice assistant ledger: calls, spoken turns with their
|
|
42
|
+
* transcripts, and the Bot delegations each made. Absent on a deployment
|
|
43
|
+
* with no voice objects; the route then 404s.
|
|
44
|
+
*/
|
|
45
|
+
voice?(userId: string): Promise<unknown>;
|
|
46
|
+
isAdminUser(userId: string): Promise<boolean>;
|
|
47
|
+
setAccountFeatures(
|
|
48
|
+
userId: string,
|
|
49
|
+
command: SetUserFeaturesCommandV1,
|
|
50
|
+
): Promise<UserFeaturesV1>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type DebugTurnSubmitter = (
|
|
54
|
+
userId: string,
|
|
55
|
+
botId: string,
|
|
56
|
+
text: string,
|
|
57
|
+
) => Promise<Response>;
|
|
58
|
+
|
|
59
|
+
const DEBUG_PREFIX = "/api/debug";
|
|
60
|
+
|
|
61
|
+
function jsonError(status: number, message: string): Response {
|
|
62
|
+
return Response.json({ error: message }, { status });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function forbiddenUser(): Response {
|
|
66
|
+
return new Response(
|
|
67
|
+
"Debug Turn submission is only allowed for an administrator account.",
|
|
68
|
+
{
|
|
69
|
+
status: 403,
|
|
70
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Length-independent, constant-time within a length: a token comparison that
|
|
77
|
+
* returns early leaks the token one character at a time.
|
|
78
|
+
*/
|
|
79
|
+
function tokenMatches(presented: string, expected: string): boolean {
|
|
80
|
+
return constantTimeEqualsV1(presented, expected);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function presentedToken(request: Request): string | undefined {
|
|
84
|
+
const header = request.headers.get("authorization");
|
|
85
|
+
if (header?.toLowerCase().startsWith("bearer ")) {
|
|
86
|
+
return header.slice("bearer ".length).trim();
|
|
87
|
+
}
|
|
88
|
+
return request.headers.get("x-frockbot-debug-token")?.trim() ?? undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The query the URL carries, decoded here rather than inside the Bot: the same
|
|
93
|
+
* decoder the Durable Object applies, run in the gateway so a query the
|
|
94
|
+
* operator got wrong is answered as a 400 and never becomes an uncaught
|
|
95
|
+
* failure in the Bot's isolate.
|
|
96
|
+
*/
|
|
97
|
+
function debugQuery(url: URL): BotDebugQueryV1 {
|
|
98
|
+
const limit = url.searchParams.get("limit");
|
|
99
|
+
const before = url.searchParams.get("before");
|
|
100
|
+
const events = url.searchParams.get("events");
|
|
101
|
+
return decodeBotDebugQueryV1({
|
|
102
|
+
schemaVersion: 1,
|
|
103
|
+
// `Number("nonsense")` is `NaN`, which the decoder refuses with the same
|
|
104
|
+
// sentence a limit past the cap gets — one message for one bad field.
|
|
105
|
+
...(limit === null ? {} : { limit: Number(limit) }),
|
|
106
|
+
...(before === null ? {} : { before }),
|
|
107
|
+
...(events === null ? {} : { events: events === "true" || events === "1" }),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function debugTurnText(input: unknown): string {
|
|
112
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
113
|
+
throw new Error("debug Turn body must be an object");
|
|
114
|
+
}
|
|
115
|
+
const body = input as Record<string, unknown>;
|
|
116
|
+
if (Object.keys(body).some((key) => key !== "text")) {
|
|
117
|
+
throw new Error("debug Turn body only accepts text");
|
|
118
|
+
}
|
|
119
|
+
if (typeof body.text !== "string") {
|
|
120
|
+
throw new Error("debug Turn body.text must be a string");
|
|
121
|
+
}
|
|
122
|
+
return body.text;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Mounted ahead of authentication in the gateway, so it answers with no
|
|
127
|
+
* session. Returns `undefined` for any path it does not own.
|
|
128
|
+
*/
|
|
129
|
+
export function createDebugRoute(
|
|
130
|
+
surface: DebugGatewaySurface | undefined,
|
|
131
|
+
submitTurn?: DebugTurnSubmitter,
|
|
132
|
+
): (request: Request, url: URL) => Promise<Response | undefined> {
|
|
133
|
+
return async (request, url) => {
|
|
134
|
+
if (
|
|
135
|
+
url.pathname !== DEBUG_PREFIX &&
|
|
136
|
+
!url.pathname.startsWith(`${DEBUG_PREFIX}/`)
|
|
137
|
+
) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
// An unconfigured deployment does not admit that the surface exists.
|
|
141
|
+
if (!surface?.token) return jsonError(404, "not found");
|
|
142
|
+
const presented = presentedToken(request);
|
|
143
|
+
if (!presented || !tokenMatches(presented, surface.token)) {
|
|
144
|
+
return jsonError(401, "debug token required");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const path = url.pathname.slice(DEBUG_PREFIX.length);
|
|
148
|
+
try {
|
|
149
|
+
const sendMatch = path.match(/^\/users\/([^/]+)\/bots\/([^/]+)\/turns$/);
|
|
150
|
+
if (sendMatch) {
|
|
151
|
+
if (request.method !== "POST") {
|
|
152
|
+
return jsonError(405, "method not allowed");
|
|
153
|
+
}
|
|
154
|
+
const userId = decodeURIComponent(sendMatch[1]!);
|
|
155
|
+
const botId = decodeBotIdV1(decodeURIComponent(sendMatch[2]!));
|
|
156
|
+
// Resolve the durable sign-in identity before reading or forwarding
|
|
157
|
+
// the body. The debug token alone never gains authority to speak as an
|
|
158
|
+
// arbitrary User, and a User with no identity row is denied too.
|
|
159
|
+
if (!(await surface.isAdminUser(userId))) return forbiddenUser();
|
|
160
|
+
let text: string;
|
|
161
|
+
try {
|
|
162
|
+
text = debugTurnText(await request.json());
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return jsonError(
|
|
165
|
+
400,
|
|
166
|
+
error instanceof Error
|
|
167
|
+
? error.message
|
|
168
|
+
: "debug Turn body is invalid",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
// This is the only write on `/api/debug`: `submitTurn` forwards an
|
|
172
|
+
// ordinary client command to the loaded User application, so the
|
|
173
|
+
// response (including a 409 admission refusal or a 413 size refusal)
|
|
174
|
+
// is returned byte-for-byte with its original status.
|
|
175
|
+
if (!submitTurn)
|
|
176
|
+
return jsonError(503, "debug Turn send is unavailable");
|
|
177
|
+
return submitTurn(userId, botId, text);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const featuresMatch = path.match(/^\/users\/([^/]+)\/features$/);
|
|
181
|
+
if (featuresMatch) {
|
|
182
|
+
if (request.method !== "POST") {
|
|
183
|
+
return jsonError(405, "method not allowed");
|
|
184
|
+
}
|
|
185
|
+
const userId = decodeURIComponent(featuresMatch[1]!);
|
|
186
|
+
let command: SetUserFeaturesCommandV1;
|
|
187
|
+
try {
|
|
188
|
+
command = decodeSetUserFeaturesCommandV1(await request.json());
|
|
189
|
+
} catch (error) {
|
|
190
|
+
return jsonError(
|
|
191
|
+
400,
|
|
192
|
+
error instanceof Error
|
|
193
|
+
? error.message
|
|
194
|
+
: "account features command is invalid",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
// The account is not provisioned or admitted by this write: the record
|
|
198
|
+
// is the account's own, and an account that never signs in keeps it.
|
|
199
|
+
return Response.json(await surface.setAccountFeatures(userId, command));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Every other debug route is read-only.
|
|
203
|
+
if (request.method !== "GET") return jsonError(405, "method not allowed");
|
|
204
|
+
if (path === "" || path === "/") {
|
|
205
|
+
return Response.json({
|
|
206
|
+
schemaVersion: 1,
|
|
207
|
+
routes: [
|
|
208
|
+
"GET /api/debug/users",
|
|
209
|
+
"GET /api/debug/bots?userId=<id>",
|
|
210
|
+
"GET /api/debug/bots/<botId>?userId=<id>&limit=<n>&events=true&before=<cursor>",
|
|
211
|
+
"GET /api/debug/bots/<botId>/runs/<runId>?userId=<id>",
|
|
212
|
+
"GET /api/debug/voice?userId=<id>",
|
|
213
|
+
"POST /api/debug/users/<userId>/bots/<botId>/turns",
|
|
214
|
+
"POST /api/debug/users/<userId>/features",
|
|
215
|
+
],
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (path === "/users") {
|
|
219
|
+
return Response.json({
|
|
220
|
+
schemaVersion: 1,
|
|
221
|
+
users: await surface.listUsers(),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const userId = url.searchParams.get("userId")?.trim();
|
|
226
|
+
if (path === "/bots") {
|
|
227
|
+
if (!userId) return jsonError(400, "userId is required");
|
|
228
|
+
return Response.json(await surface.listBots(userId));
|
|
229
|
+
}
|
|
230
|
+
if (path === "/voice") {
|
|
231
|
+
if (!surface.voice) return jsonError(404, "not found");
|
|
232
|
+
if (!userId) return jsonError(400, "userId is required");
|
|
233
|
+
return Response.json(await surface.voice(userId));
|
|
234
|
+
}
|
|
235
|
+
const runMatch = path.match(/^\/bots\/([^/]+)\/runs\/([^/]+)$/);
|
|
236
|
+
const botMatch = path.match(/^\/bots\/([^/]+)$/);
|
|
237
|
+
if (!runMatch && !botMatch) return jsonError(404, "not found");
|
|
238
|
+
if (!userId) return jsonError(400, "userId is required");
|
|
239
|
+
const botId = decodeBotIdV1(
|
|
240
|
+
decodeURIComponent((runMatch ?? botMatch)![1]!),
|
|
241
|
+
);
|
|
242
|
+
const query = runMatch
|
|
243
|
+
? decodeBotDebugQueryV1({
|
|
244
|
+
schemaVersion: 1,
|
|
245
|
+
runId: decodeURIComponent(runMatch[2]!),
|
|
246
|
+
})
|
|
247
|
+
: debugQuery(url);
|
|
248
|
+
return Response.json(await surface.snapshot(userId, botId, query));
|
|
249
|
+
} catch (error) {
|
|
250
|
+
// A query the caller got wrong is their 400, not the deployment's 500 —
|
|
251
|
+
// and the message says the range so the next attempt can be right.
|
|
252
|
+
if (isBotDebugQueryRefusalV1(error)) {
|
|
253
|
+
return jsonError(
|
|
254
|
+
400,
|
|
255
|
+
error instanceof Error ? error.message : "debug query is invalid",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
// The operator is the audience: the message is the finding, and a stack
|
|
259
|
+
// that reached here is more useful than a generic 500.
|
|
260
|
+
return Response.json(
|
|
261
|
+
{
|
|
262
|
+
error:
|
|
263
|
+
error instanceof Error ? error.message : "debug request failed",
|
|
264
|
+
...(error instanceof Error && error.stack
|
|
265
|
+
? { stack: error.stack }
|
|
266
|
+
: {}),
|
|
267
|
+
},
|
|
268
|
+
{ status: 500 },
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Disposable pre-user cleanup for retired default-Package bootstrap markers.
|
|
2
|
+
//
|
|
3
|
+
// Markers v1–v3 are `{ schemaVersion: 1 | 2 | 3 }` with no ledger. The decoder
|
|
4
|
+
// no longer accepts them. This pass deletes those records before any request
|
|
5
|
+
// or alarm can read settings, so the next read is the no-marker seed. A v4
|
|
6
|
+
// ledger is left alone. The receipt makes a second load free.
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_PACKAGES_MARKER_KEY = "user-default-packages-bootstrap:v1";
|
|
9
|
+
export const DEFAULT_PACKAGES_MARKER_CLEANUP_RECEIPT_KEY =
|
|
10
|
+
"maintenance:default-packages-marker:2026-09-20";
|
|
11
|
+
|
|
12
|
+
export function isRetiredDefaultPackagesMarker(value: unknown): boolean {
|
|
13
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const keys = Object.keys(value);
|
|
17
|
+
if (keys.length !== 1 || keys[0] !== "schemaVersion") return false;
|
|
18
|
+
const version = (value as { schemaVersion?: unknown }).schemaVersion;
|
|
19
|
+
return version === 1 || version === 2 || version === 3;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function cleanDefaultPackagesMarkerV1(
|
|
23
|
+
storage: DurableObjectStorage,
|
|
24
|
+
now: Date = new Date(),
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
if (await storage.get(DEFAULT_PACKAGES_MARKER_CLEANUP_RECEIPT_KEY)) return;
|
|
27
|
+
await storage.transaction(async (tx) => {
|
|
28
|
+
if (await tx.get(DEFAULT_PACKAGES_MARKER_CLEANUP_RECEIPT_KEY)) return;
|
|
29
|
+
const marker = await tx.get<unknown>(DEFAULT_PACKAGES_MARKER_KEY);
|
|
30
|
+
const removed = isRetiredDefaultPackagesMarker(marker);
|
|
31
|
+
if (removed) {
|
|
32
|
+
await tx.delete(DEFAULT_PACKAGES_MARKER_KEY);
|
|
33
|
+
}
|
|
34
|
+
await tx.put(DEFAULT_PACKAGES_MARKER_CLEANUP_RECEIPT_KEY, {
|
|
35
|
+
at: now.toISOString(),
|
|
36
|
+
removed,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { AdminOperationsHostV1 } from "@frockbot/app/admin/operations";
|
|
2
|
+
import {
|
|
3
|
+
AccountAccessConflictError,
|
|
4
|
+
decodeAccountAccessV1,
|
|
5
|
+
decodeAccountAccessViewV1,
|
|
6
|
+
decodeAdminWriteResultV1,
|
|
7
|
+
decodeDeploymentPolicyV1,
|
|
8
|
+
decodeEmailInvitationV1,
|
|
9
|
+
DeploymentPolicyConflictError,
|
|
10
|
+
} from "@frockbot/app/admin/shared";
|
|
11
|
+
import {
|
|
12
|
+
decodeHostedModelRatesV1,
|
|
13
|
+
decodeHostedModelRatesViewV1,
|
|
14
|
+
ModelRatesConflictError,
|
|
15
|
+
} from "@frockbot/app/billing/rates";
|
|
16
|
+
import { rpcJsonSnapshotV1 } from "./durable-rpc.js";
|
|
17
|
+
|
|
18
|
+
interface DeploymentPolicyAdminRpc {
|
|
19
|
+
readPolicy(input: unknown): Promise<unknown>;
|
|
20
|
+
setAdmissionMode(input: unknown): Promise<unknown>;
|
|
21
|
+
readAccountAccess(input: unknown): Promise<unknown>;
|
|
22
|
+
setAccountAccess(input: unknown): Promise<unknown>;
|
|
23
|
+
inviteEmail(input: unknown): Promise<unknown>;
|
|
24
|
+
readModelRatesView(input: unknown): Promise<unknown>;
|
|
25
|
+
saveModelRates(input: unknown): Promise<unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function appliedWrite(
|
|
29
|
+
answer: unknown,
|
|
30
|
+
conflict: (currentRevision: number) => Error,
|
|
31
|
+
): unknown {
|
|
32
|
+
const write = rpcJsonSnapshotV1(answer) as Record<string, unknown> | null;
|
|
33
|
+
let decoded;
|
|
34
|
+
try {
|
|
35
|
+
decoded = decodeAdminWriteResultV1(
|
|
36
|
+
write,
|
|
37
|
+
(value) => value,
|
|
38
|
+
"access authority write result",
|
|
39
|
+
);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error("access authority answered an unknown write result");
|
|
42
|
+
}
|
|
43
|
+
if (decoded.status === "conflict") throw conflict(decoded.currentRevision);
|
|
44
|
+
return decoded.value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createDeploymentPolicyAdminHost(
|
|
48
|
+
authority: () => DeploymentPolicyAdminRpc,
|
|
49
|
+
): Pick<
|
|
50
|
+
AdminOperationsHostV1,
|
|
51
|
+
| "readDeploymentPolicy"
|
|
52
|
+
| "setAdmissionMode"
|
|
53
|
+
| "readAccountAccess"
|
|
54
|
+
| "setAccountAccess"
|
|
55
|
+
| "inviteEmail"
|
|
56
|
+
| "readModelRates"
|
|
57
|
+
| "saveModelRates"
|
|
58
|
+
> {
|
|
59
|
+
return {
|
|
60
|
+
readDeploymentPolicy: async () =>
|
|
61
|
+
decodeDeploymentPolicyV1(
|
|
62
|
+
rpcJsonSnapshotV1(await authority().readPolicy({ schemaVersion: 1 })),
|
|
63
|
+
),
|
|
64
|
+
setAdmissionMode: async (command, updatedBy) =>
|
|
65
|
+
decodeDeploymentPolicyV1(
|
|
66
|
+
appliedWrite(
|
|
67
|
+
await authority().setAdmissionMode({
|
|
68
|
+
schemaVersion: 1,
|
|
69
|
+
command,
|
|
70
|
+
updatedBy,
|
|
71
|
+
}),
|
|
72
|
+
(revision) => new DeploymentPolicyConflictError(revision),
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
readAccountAccess: async (userId) =>
|
|
76
|
+
decodeAccountAccessViewV1(
|
|
77
|
+
rpcJsonSnapshotV1(
|
|
78
|
+
await authority().readAccountAccess({ schemaVersion: 1, userId }),
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
setAccountAccess: async (userId, command, updatedBy) =>
|
|
82
|
+
decodeAccountAccessV1(
|
|
83
|
+
appliedWrite(
|
|
84
|
+
await authority().setAccountAccess({
|
|
85
|
+
schemaVersion: 1,
|
|
86
|
+
userId,
|
|
87
|
+
command,
|
|
88
|
+
updatedBy,
|
|
89
|
+
}),
|
|
90
|
+
(revision) => new AccountAccessConflictError(revision),
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
inviteEmail: async (command, invitedBy) =>
|
|
94
|
+
decodeEmailInvitationV1(
|
|
95
|
+
rpcJsonSnapshotV1(
|
|
96
|
+
await authority().inviteEmail({
|
|
97
|
+
schemaVersion: 1,
|
|
98
|
+
command,
|
|
99
|
+
invitedBy,
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
readModelRates: async () =>
|
|
104
|
+
decodeHostedModelRatesViewV1(
|
|
105
|
+
rpcJsonSnapshotV1(
|
|
106
|
+
await authority().readModelRatesView({ schemaVersion: 1 }),
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
saveModelRates: async (command, createdBy) =>
|
|
110
|
+
decodeHostedModelRatesV1(
|
|
111
|
+
appliedWrite(
|
|
112
|
+
await authority().saveModelRates({
|
|
113
|
+
schemaVersion: 1,
|
|
114
|
+
command,
|
|
115
|
+
createdBy,
|
|
116
|
+
}),
|
|
117
|
+
(revision) => new ModelRatesConflictError(revision),
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
};
|
|
121
|
+
}
|