@opengeni/api-router 0.23.1 → 0.30.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/dist/app.d.ts +3 -2
- package/dist/app.js +5 -3
- package/dist/auth/canonical-human-session-admission.d.ts +27 -0
- package/dist/browser-auth-broker.d.ts +20 -0
- package/dist/browser-controller-authority.d.ts +61 -0
- package/dist/browser-network-route.d.ts +8 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-T4T2PGU4.js → chunk-2PQMSRCB.js} +37891 -17463
- package/dist/chunk-2PQMSRCB.js.map +1 -0
- package/dist/codemode.d.ts +23 -0
- package/dist/controller-data-plane.d.ts +12 -0
- package/dist/editable-artifact-live-hints.d.ts +11 -0
- package/dist/editable-artifact-native-kernel.d.ts +37 -0
- package/dist/editable-artifact-office-import.d.ts +22 -0
- package/dist/editable-artifact-production.d.ts +29 -0
- package/dist/editable-artifact-websocket.d.ts +49 -0
- package/dist/editable-artifact-workspace-files.d.ts +23 -0
- package/dist/github-browser-flow.d.ts +6 -0
- package/dist/http/auth.d.ts +1 -1
- package/dist/http/cors.d.ts +1 -0
- package/dist/http/sse.d.ts +18 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1796 -22
- package/dist/index.js.map +1 -1
- package/dist/integrations/api-integrations.d.ts +24 -0
- package/dist/integrations/atlassian.d.ts +176 -0
- package/dist/integrations/fiken.d.ts +233 -0
- package/dist/integrations/github-skill-source.d.ts +5 -0
- package/dist/integrations/google-drive.d.ts +85 -0
- package/dist/integrations/oauth-client.d.ts +22 -1
- package/dist/integrations/provider-oauth.d.ts +17 -0
- package/dist/integrations/slack-bot.d.ts +175 -1
- package/dist/integrations/slack-interactions.d.ts +24 -6
- package/dist/integrations/social-api.d.ts +2 -1
- package/dist/interaction-holder-heartbeat.d.ts +17 -0
- package/dist/interaction-metrics.d.ts +11 -0
- package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
- package/dist/mcp/editable-artifacts.d.ts +13 -0
- package/dist/mcp/scheduled-task-view.d.ts +168 -0
- package/dist/mcp/server.d.ts +16 -4
- package/dist/memory-slack-delivery.d.ts +9 -0
- package/dist/model-catalog.d.ts +1 -0
- package/dist/routes/api-integrations.d.ts +8 -0
- package/dist/routes/browser-identities.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +6 -0
- package/dist/routes/canonical-human-identities.d.ts +3 -0
- package/dist/routes/channels.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +3 -0
- package/dist/routes/computer-sessions.d.ts +6 -0
- package/dist/routes/editable-artifacts.d.ts +44 -0
- package/dist/routes/integration-facets.d.ts +3 -0
- package/dist/routes/interaction-resources.d.ts +5 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/organization-memberships.d.ts +3 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +18 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/slack-task-policy.d.ts +3 -0
- package/dist/routes/supergrok.d.ts +3 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/channel-a.d.ts +62 -2
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +7 -3
- package/dist/slack-reaction-files.d.ts +27 -0
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/dist/transcription/providers/xai-subscription.d.ts +8 -0
- package/dist/xai-realtime.d.ts +26 -0
- package/dist/xai-subscription-auth.d.ts +23 -0
- package/package.json +20 -14
- package/src/app.ts +604 -64
- package/src/auth/canonical-human-session-admission.ts +116 -0
- package/src/auth/managed-auth.ts +101 -0
- package/src/browser-auth-broker.ts +155 -0
- package/src/browser-controller-authority.ts +186 -0
- package/src/browser-network-route.ts +34 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -0
- package/src/controller-data-plane.ts +35 -0
- package/src/editable-artifact-live-hints.ts +64 -0
- package/src/editable-artifact-native-kernel.ts +659 -0
- package/src/editable-artifact-office-import.ts +230 -0
- package/src/editable-artifact-production.ts +419 -0
- package/src/editable-artifact-websocket.ts +311 -0
- package/src/editable-artifact-workspace-files.ts +186 -0
- package/src/github-browser-flow.ts +35 -6
- package/src/http/auth.ts +29 -9
- package/src/http/cors.ts +3 -0
- package/src/http/sse.ts +229 -5
- package/src/index.ts +119 -20
- package/src/integrations/api-integrations.ts +365 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/fiken.ts +1230 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1005 -64
- package/src/integrations/oauth-client.ts +123 -33
- package/src/integrations/provider-oauth.ts +756 -0
- package/src/integrations/slack-bot.ts +669 -11
- package/src/integrations/slack-interactions.ts +2063 -93
- package/src/integrations/social-api.ts +11 -0
- package/src/interaction-holder-heartbeat.ts +95 -0
- package/src/interaction-metrics.ts +159 -0
- package/src/mcp/documents.ts +146 -6
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/scheduled-task-view.ts +34 -0
- package/src/mcp/server.ts +1756 -355
- package/src/memory-slack-delivery.ts +209 -0
- package/src/model-catalog.ts +19 -6
- package/src/routes/api-integrations.ts +410 -0
- package/src/routes/browser-identities.ts +160 -0
- package/src/routes/browser-sessions.ts +4482 -0
- package/src/routes/canonical-human-identities.ts +156 -0
- package/src/routes/channels.ts +90 -0
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1602 -0
- package/src/routes/connections.ts +421 -99
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/files.ts +106 -4
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +7 -1
- package/src/routes/integration-facets.ts +258 -0
- package/src/routes/interaction-resources.ts +595 -0
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/organization-memberships.ts +53 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +752 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +67 -40
- package/src/routes/sessions.ts +830 -278
- package/src/routes/skills.ts +256 -0
- package/src/routes/slack-task-policy.ts +115 -0
- package/src/routes/supergrok.ts +717 -0
- package/src/routes/transcription-recordings.ts +9 -2
- package/src/routes/transcriptions.ts +2 -1
- package/src/routes/video-generation.ts +162 -0
- package/src/routes/workspaces.ts +68 -28
- package/src/sandbox/channel-a.ts +750 -116
- package/src/sandbox/machines.ts +5 -5
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +39 -12
- package/src/slack-reaction-files.ts +181 -0
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/src/transcription/providers/xai-subscription.ts +110 -0
- package/src/transcription/service.ts +17 -2
- package/src/xai-realtime.ts +216 -0
- package/src/xai-subscription-auth.ts +132 -0
- package/dist/chunk-T4T2PGU4.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -71
- package/src/mcp/toolspace.ts +0 -1190
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EditableArtifactDomainError,
|
|
3
|
+
EditableArtifactLiveError,
|
|
4
|
+
decodeEditableArtifactLiveClientWireFrame,
|
|
5
|
+
encodeEditableArtifactLiveServerWireFrame,
|
|
6
|
+
type EditableArtifactApplicationPort,
|
|
7
|
+
type EditableArtifactLiveClose,
|
|
8
|
+
type EditableArtifactLiveServerFrame,
|
|
9
|
+
type EditableArtifactLiveSession,
|
|
10
|
+
type EditableArtifactLiveSinkPort,
|
|
11
|
+
} from "@opengeni/core";
|
|
12
|
+
|
|
13
|
+
export const EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PATH = "/v1/editable-artifacts/live";
|
|
14
|
+
export const EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PROTOCOL = "opengeni-artifact-v1";
|
|
15
|
+
export const EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES = 8 * 1024 * 1024 + 64 * 1024;
|
|
16
|
+
|
|
17
|
+
const MAX_QUEUED_MESSAGES = 64;
|
|
18
|
+
const MAX_QUEUED_BYTES = 12 * 1024 * 1024;
|
|
19
|
+
|
|
20
|
+
export type EditableArtifactWebSocketLike = Readonly<{
|
|
21
|
+
data: EditableArtifactWebSocketConnection;
|
|
22
|
+
send(data: Uint8Array, compress?: boolean): number;
|
|
23
|
+
close(code?: number, reason?: string): void;
|
|
24
|
+
readonly bufferedAmount?: number;
|
|
25
|
+
getBufferedAmount?(): number;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
export type EditableArtifactWebSocketUpgradeServer = Readonly<{
|
|
29
|
+
upgrade(
|
|
30
|
+
request: Request,
|
|
31
|
+
options: Readonly<{
|
|
32
|
+
data: EditableArtifactWebSocketConnection;
|
|
33
|
+
headers: HeadersInit;
|
|
34
|
+
}>,
|
|
35
|
+
): boolean;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
export type EditableArtifactWebSocketHandler = Readonly<{
|
|
39
|
+
open(socket: EditableArtifactWebSocketLike): void;
|
|
40
|
+
message(socket: EditableArtifactWebSocketLike, message: string | Uint8Array | ArrayBuffer): void;
|
|
41
|
+
close(socket: EditableArtifactWebSocketLike): void;
|
|
42
|
+
}>;
|
|
43
|
+
|
|
44
|
+
export class EditableArtifactWebSocketTransport {
|
|
45
|
+
readonly websocket: EditableArtifactWebSocketHandler;
|
|
46
|
+
|
|
47
|
+
constructor(private readonly application: EditableArtifactApplicationPort | undefined) {
|
|
48
|
+
this.websocket = Object.freeze({
|
|
49
|
+
open: (socket) => socket.data.attach(socket),
|
|
50
|
+
message: (socket, message) => socket.data.receive(message),
|
|
51
|
+
close: (socket) => socket.data.transportClosed(),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
handles(request: Request): boolean {
|
|
56
|
+
return new URL(request.url).pathname === EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PATH;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
upgrade(request: Request, server: EditableArtifactWebSocketUpgradeServer): Response | undefined {
|
|
60
|
+
const url = new URL(request.url);
|
|
61
|
+
if (url.pathname !== EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PATH) return undefined;
|
|
62
|
+
if (request.method !== "GET") {
|
|
63
|
+
return new Response("Method Not Allowed", {
|
|
64
|
+
status: 405,
|
|
65
|
+
headers: { allow: "GET" },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (!this.application) return new Response("Service Unavailable", { status: 503 });
|
|
69
|
+
if (!offeredProtocol(request.headers.get("sec-websocket-protocol"))) {
|
|
70
|
+
return new Response("WebSocket protocol required", { status: 426 });
|
|
71
|
+
}
|
|
72
|
+
const connection = new EditableArtifactWebSocketConnection(this.application);
|
|
73
|
+
const upgraded = server.upgrade(request, {
|
|
74
|
+
data: connection,
|
|
75
|
+
headers: {
|
|
76
|
+
"sec-websocket-protocol": EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PROTOCOL,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
return upgraded ? undefined : new Response("Bad Request", { status: 400 });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One socket, one consumed ticket, one stream epoch. */
|
|
84
|
+
export class EditableArtifactWebSocketConnection implements EditableArtifactLiveSinkPort {
|
|
85
|
+
private socket: EditableArtifactWebSocketLike | null = null;
|
|
86
|
+
private session: EditableArtifactLiveSession | null = null;
|
|
87
|
+
private work: Promise<void> = Promise.resolve();
|
|
88
|
+
private queuedMessages = 0;
|
|
89
|
+
private queuedBytes = 0;
|
|
90
|
+
private terminal = false;
|
|
91
|
+
|
|
92
|
+
constructor(private readonly application: EditableArtifactApplicationPort) {}
|
|
93
|
+
|
|
94
|
+
attach(socket: EditableArtifactWebSocketLike): void {
|
|
95
|
+
if (this.socket || this.terminal) {
|
|
96
|
+
socket.close(4400, "invalid connection state");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.socket = socket;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
receive(message: string | Uint8Array | ArrayBuffer): void {
|
|
103
|
+
if (this.terminal) return;
|
|
104
|
+
if (typeof message === "string") {
|
|
105
|
+
void this.fail("invalid_frame");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const byteLength = message.byteLength;
|
|
109
|
+
if (
|
|
110
|
+
byteLength === 0 ||
|
|
111
|
+
byteLength > EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES ||
|
|
112
|
+
this.queuedMessages + 1 > MAX_QUEUED_MESSAGES ||
|
|
113
|
+
this.queuedBytes + byteLength > MAX_QUEUED_BYTES
|
|
114
|
+
) {
|
|
115
|
+
void this.fail(
|
|
116
|
+
byteLength > EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES
|
|
117
|
+
? "oversized_frame"
|
|
118
|
+
: "slow_client",
|
|
119
|
+
);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const bytes =
|
|
123
|
+
message instanceof Uint8Array ? Uint8Array.from(message) : new Uint8Array(message).slice();
|
|
124
|
+
this.queuedMessages += 1;
|
|
125
|
+
this.queuedBytes += bytes.byteLength;
|
|
126
|
+
const run = this.work.then(async () => {
|
|
127
|
+
try {
|
|
128
|
+
await this.process(bytes);
|
|
129
|
+
} finally {
|
|
130
|
+
this.queuedMessages -= 1;
|
|
131
|
+
this.queuedBytes -= bytes.byteLength;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
this.work = run.catch(async (error: unknown) => {
|
|
135
|
+
await this.handleFailure(error);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async send(frame: EditableArtifactLiveServerFrame): Promise<void> {
|
|
140
|
+
if (this.terminal || !this.socket) {
|
|
141
|
+
throw new EditableArtifactLiveError("closed", "Editable artifact socket is closed");
|
|
142
|
+
}
|
|
143
|
+
const bytes = encodeEditableArtifactLiveServerWireFrame(frame);
|
|
144
|
+
if (bytes.byteLength > EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES) {
|
|
145
|
+
throw new EditableArtifactLiveError(
|
|
146
|
+
"oversized_frame",
|
|
147
|
+
"Editable artifact server frame exceeds socket limit",
|
|
148
|
+
{ requiresSnapshot: true },
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const accepted = this.socket.send(bytes, false);
|
|
152
|
+
if (!Number.isSafeInteger(accepted) || accepted <= 0) {
|
|
153
|
+
throw new EditableArtifactLiveError("closed", "Editable artifact socket rejected frame");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
bufferedBytes(): number {
|
|
158
|
+
const buffered = this.socket?.getBufferedAmount?.() ?? this.socket?.bufferedAmount ?? 0;
|
|
159
|
+
return Number.isSafeInteger(buffered) && buffered >= 0 ? buffered : Number.MAX_SAFE_INTEGER;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
close(close: EditableArtifactLiveClose): void {
|
|
163
|
+
if (this.terminal) return;
|
|
164
|
+
this.terminal = true;
|
|
165
|
+
this.socket?.close(closeCode(close.reason), close.reason);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
transportClosed(): void {
|
|
169
|
+
if (this.terminal) return;
|
|
170
|
+
this.terminal = true;
|
|
171
|
+
void this.session?.close("transport_error");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private async process(bytes: Uint8Array): Promise<void> {
|
|
175
|
+
if (this.terminal) return;
|
|
176
|
+
const frame = decodeEditableArtifactLiveClientWireFrame(bytes);
|
|
177
|
+
if (!this.session) {
|
|
178
|
+
if (frame.type !== "open") {
|
|
179
|
+
throw new EditableArtifactLiveError("invalid_frame", "First socket frame must be open");
|
|
180
|
+
}
|
|
181
|
+
const session = await this.application.openLive({
|
|
182
|
+
token: frame.token,
|
|
183
|
+
artifactId: frame.artifactId,
|
|
184
|
+
protocolVersion: frame.protocolVersion,
|
|
185
|
+
resume: frame.resume,
|
|
186
|
+
sink: this,
|
|
187
|
+
});
|
|
188
|
+
if (this.terminal) {
|
|
189
|
+
await session.close("transport_error");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this.session = session;
|
|
193
|
+
void session.closed.then(() => {
|
|
194
|
+
if (!this.terminal)
|
|
195
|
+
this.close({
|
|
196
|
+
reason: "closed",
|
|
197
|
+
retryable: false,
|
|
198
|
+
requiresSnapshot: false,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (frame.type === "open") {
|
|
204
|
+
throw new EditableArtifactLiveError("invalid_frame", "Socket is already open");
|
|
205
|
+
}
|
|
206
|
+
if (
|
|
207
|
+
frame.artifactId !== this.session.artifactId ||
|
|
208
|
+
frame.streamEpoch !== this.session.streamEpoch
|
|
209
|
+
) {
|
|
210
|
+
throw new EditableArtifactLiveError("stale_epoch", "Socket frame belongs to another stream");
|
|
211
|
+
}
|
|
212
|
+
if (frame.type === "applied") {
|
|
213
|
+
await this.session.acknowledge(frame);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
let receipt: Awaited<ReturnType<EditableArtifactLiveSession["submitIntent"]>>;
|
|
217
|
+
try {
|
|
218
|
+
receipt = await this.session.submitIntent(frame);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
await this.send({
|
|
221
|
+
type: "mutationRejected",
|
|
222
|
+
protocolVersion: frame.protocolVersion,
|
|
223
|
+
artifactId: frame.artifactId,
|
|
224
|
+
streamEpoch: frame.streamEpoch,
|
|
225
|
+
requestHash: frame.requestHash,
|
|
226
|
+
...mutationFailure(error),
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// Always return the exact durable canonical transaction evidence (OGACO
|
|
231
|
+
// or OGAST) on this request's socket, including idempotent unknown-outcome
|
|
232
|
+
// retries whose original fanout was already replayed and ACKed. Transaction
|
|
233
|
+
// first, mapping second: acceptance is never exposed without settle bytes.
|
|
234
|
+
await this.send({
|
|
235
|
+
type: "transaction",
|
|
236
|
+
protocolVersion: frame.protocolVersion,
|
|
237
|
+
artifactId: frame.artifactId,
|
|
238
|
+
streamEpoch: frame.streamEpoch,
|
|
239
|
+
transaction: receipt.transaction,
|
|
240
|
+
});
|
|
241
|
+
await this.send({
|
|
242
|
+
type: "mutationAccepted",
|
|
243
|
+
protocolVersion: frame.protocolVersion,
|
|
244
|
+
artifactId: frame.artifactId,
|
|
245
|
+
streamEpoch: frame.streamEpoch,
|
|
246
|
+
requestHash: receipt.requestHash,
|
|
247
|
+
clientTransactionId: receipt.clientTransactionId,
|
|
248
|
+
transactionId: receipt.transaction.transactionId,
|
|
249
|
+
startSequence: receipt.transaction.startSequence,
|
|
250
|
+
endSequence: receipt.transaction.endSequence,
|
|
251
|
+
stateHash: receipt.transaction.stateHash,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private async handleFailure(error: unknown): Promise<void> {
|
|
256
|
+
if (this.terminal) return;
|
|
257
|
+
if (error instanceof EditableArtifactLiveError) {
|
|
258
|
+
await this.fail(error.code === "invalid_ticket" ? "transport_error" : error.code);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
await this.fail("transport_error");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private async fail(reason: Parameters<EditableArtifactLiveSession["close"]>[0]): Promise<void> {
|
|
265
|
+
if (this.terminal) return;
|
|
266
|
+
this.terminal = true;
|
|
267
|
+
await this.session?.close(reason).catch(() => undefined);
|
|
268
|
+
this.socket?.close(closeCode(reason ?? "transport_error"), reason ?? "transport_error");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function mutationFailure(error: unknown): Readonly<{
|
|
273
|
+
code: "invalid_request" | "forbidden" | "conflict" | "unsupported" | "unavailable";
|
|
274
|
+
retryable: boolean;
|
|
275
|
+
}> {
|
|
276
|
+
if (error instanceof EditableArtifactDomainError) {
|
|
277
|
+
if (error.code === "forbidden") return { code: "forbidden", retryable: false };
|
|
278
|
+
if (["invalid_request", "request_hash_mismatch", "invalid_undo_target"].includes(error.code)) {
|
|
279
|
+
return { code: "invalid_request", retryable: false };
|
|
280
|
+
}
|
|
281
|
+
if (error.code === "kernel_contract_violation") {
|
|
282
|
+
return { code: "unsupported", retryable: false };
|
|
283
|
+
}
|
|
284
|
+
if (["retryable_conflict", "outbox_lease_conflict"].includes(error.code)) {
|
|
285
|
+
return { code: "conflict", retryable: true };
|
|
286
|
+
}
|
|
287
|
+
return { code: "conflict", retryable: false };
|
|
288
|
+
}
|
|
289
|
+
if (error instanceof EditableArtifactLiveError && error.code === "permission_changed") {
|
|
290
|
+
return { code: "forbidden", retryable: false };
|
|
291
|
+
}
|
|
292
|
+
return { code: "unavailable", retryable: true };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function offeredProtocol(value: string | null): boolean {
|
|
296
|
+
return (
|
|
297
|
+
value
|
|
298
|
+
?.split(",")
|
|
299
|
+
.map((part) => part.trim())
|
|
300
|
+
.includes(EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PROTOCOL) ?? false
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function closeCode(reason: string): number {
|
|
305
|
+
if (reason === "permission_changed") return 4403;
|
|
306
|
+
if (reason === "ticket_expired" || reason === "ticket_replayed") return 4401;
|
|
307
|
+
if (reason === "protocol_mismatch") return 4406;
|
|
308
|
+
if (reason === "slow_client" || reason === "oversized_frame") return 4409;
|
|
309
|
+
if (reason === "closed") return 1000;
|
|
310
|
+
return 4400;
|
|
311
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type EditableArtifactAgentWorkspaceFilePort,
|
|
5
|
+
type EditableArtifactDurableExportService,
|
|
6
|
+
} from "@opengeni/core/editable-artifacts";
|
|
7
|
+
import { completeFileUpload, prepareGeneratedWorkspaceFile, type Database } from "@opengeni/db";
|
|
8
|
+
import type { ObjectHead, ObjectStorage } from "@opengeni/storage";
|
|
9
|
+
|
|
10
|
+
const UPLOAD_INTENT_TTL_MS = 60 * 60_000;
|
|
11
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
12
|
+
|
|
13
|
+
export type EditableArtifactWorkspaceFileAdapterDependencies = Readonly<{
|
|
14
|
+
db: Database;
|
|
15
|
+
objectStorage: ObjectStorage;
|
|
16
|
+
durableExports: EditableArtifactDurableExportService;
|
|
17
|
+
now?: () => Date;
|
|
18
|
+
prepareFile?: typeof prepareGeneratedWorkspaceFile;
|
|
19
|
+
completeFile?: typeof completeFileUpload;
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Promotes one immutable materialization into the normal workspace-file domain.
|
|
24
|
+
* The returned file ID is the only artifact export handle exposed to agents.
|
|
25
|
+
*/
|
|
26
|
+
export class EditableArtifactWorkspaceFileAdapter implements EditableArtifactAgentWorkspaceFilePort {
|
|
27
|
+
private readonly now: () => Date;
|
|
28
|
+
private readonly prepareFile: typeof prepareGeneratedWorkspaceFile;
|
|
29
|
+
private readonly completeFile: typeof completeFileUpload;
|
|
30
|
+
|
|
31
|
+
constructor(private readonly dependencies: EditableArtifactWorkspaceFileAdapterDependencies) {
|
|
32
|
+
this.now = dependencies.now ?? (() => new Date());
|
|
33
|
+
this.prepareFile = dependencies.prepareFile ?? prepareGeneratedWorkspaceFile;
|
|
34
|
+
this.completeFile = dependencies.completeFile ?? completeFileUpload;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async ensureMaterializationFile(
|
|
38
|
+
input: Parameters<EditableArtifactAgentWorkspaceFilePort["ensureMaterializationFile"]>[0],
|
|
39
|
+
): ReturnType<EditableArtifactAgentWorkspaceFilePort["ensureMaterializationFile"]> {
|
|
40
|
+
throwIfAborted(input.signal);
|
|
41
|
+
const storage = this.dependencies.objectStorage;
|
|
42
|
+
if (!storage.headObject || !storage.putObjectStreamIfAbsent) {
|
|
43
|
+
throw new Error("Object storage lacks immutable streaming upload support");
|
|
44
|
+
}
|
|
45
|
+
const download = await this.dependencies.durableExports.openMaterializationDownload({
|
|
46
|
+
scope: input.scope,
|
|
47
|
+
actor: input.actor,
|
|
48
|
+
artifactId: input.artifact.id,
|
|
49
|
+
jobId: input.jobId,
|
|
50
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
51
|
+
});
|
|
52
|
+
try {
|
|
53
|
+
const sha256 = plainSha256(download.contentHash);
|
|
54
|
+
const filename = exportFilename(input.filename, download.format);
|
|
55
|
+
const fileId = deterministicUuid(
|
|
56
|
+
`editable-artifact-export:file:${input.scope.workspaceId}:${input.artifact.id}:${input.versionId}:${input.jobId}`,
|
|
57
|
+
);
|
|
58
|
+
const uploadId = deterministicUuid(
|
|
59
|
+
`editable-artifact-export:upload:${input.scope.workspaceId}:${input.artifact.id}:${input.versionId}:${input.jobId}`,
|
|
60
|
+
);
|
|
61
|
+
const safeFilename = `artifact-${input.artifact.id}-${input.jobId}.${download.format}`;
|
|
62
|
+
const objectKey = `workspaces/${input.scope.workspaceId}/files/${fileId}/artifact-exports/${safeFilename}`;
|
|
63
|
+
const prepared = await this.prepareFile(this.dependencies.db, {
|
|
64
|
+
accountId: input.scope.accountId,
|
|
65
|
+
workspaceId: input.scope.workspaceId,
|
|
66
|
+
fileId,
|
|
67
|
+
uploadId,
|
|
68
|
+
filename,
|
|
69
|
+
safeFilename,
|
|
70
|
+
contentType: download.mimeType,
|
|
71
|
+
sizeBytes: download.byteSize,
|
|
72
|
+
sha256,
|
|
73
|
+
bucket: storage.bucket,
|
|
74
|
+
objectKey,
|
|
75
|
+
expiresAt: new Date(this.now().getTime() + UPLOAD_INTENT_TTL_MS),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let file = prepared.file;
|
|
79
|
+
if (file.status !== "ready") {
|
|
80
|
+
const existing = await storage.headObject(objectKey);
|
|
81
|
+
if (existing) {
|
|
82
|
+
assertStoredExport(existing, download.byteSize, download.mimeType, sha256);
|
|
83
|
+
} else {
|
|
84
|
+
await storage.putObjectStreamIfAbsent({
|
|
85
|
+
key: objectKey,
|
|
86
|
+
contentType: download.mimeType,
|
|
87
|
+
chunks: download.chunks(input.signal ? { signal: input.signal } : {}),
|
|
88
|
+
byteSize: download.byteSize,
|
|
89
|
+
sha256,
|
|
90
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
91
|
+
});
|
|
92
|
+
const stored = await storage.headObject(objectKey);
|
|
93
|
+
assertStoredExport(stored, download.byteSize, download.mimeType, sha256);
|
|
94
|
+
}
|
|
95
|
+
await download.assertUnchanged(input.signal);
|
|
96
|
+
throwIfAborted(input.signal);
|
|
97
|
+
file = await this.completeFile(this.dependencies.db, input.scope.workspaceId, uploadId);
|
|
98
|
+
} else {
|
|
99
|
+
assertStoredExport(
|
|
100
|
+
await storage.headObject(objectKey),
|
|
101
|
+
download.byteSize,
|
|
102
|
+
download.mimeType,
|
|
103
|
+
sha256,
|
|
104
|
+
);
|
|
105
|
+
await download.assertUnchanged(input.signal);
|
|
106
|
+
}
|
|
107
|
+
if (
|
|
108
|
+
file.id !== fileId ||
|
|
109
|
+
file.status !== "ready" ||
|
|
110
|
+
file.filename !== filename ||
|
|
111
|
+
file.contentType !== download.mimeType ||
|
|
112
|
+
file.sizeBytes !== download.byteSize ||
|
|
113
|
+
file.sha256 !== sha256
|
|
114
|
+
) {
|
|
115
|
+
throw new Error("Generated workspace file differs from its immutable export");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
fileId: file.id,
|
|
120
|
+
filename: file.filename,
|
|
121
|
+
contentType: file.contentType,
|
|
122
|
+
sizeBytes: file.sizeBytes,
|
|
123
|
+
sha256,
|
|
124
|
+
artifactId: input.artifact.id,
|
|
125
|
+
versionId: input.versionId,
|
|
126
|
+
materializationJobId: input.jobId,
|
|
127
|
+
sourceHeadSequence: input.sourceHeadSequence,
|
|
128
|
+
sourceStateHash: input.sourceStateHash,
|
|
129
|
+
});
|
|
130
|
+
} finally {
|
|
131
|
+
await download.close();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function assertStoredExport(
|
|
137
|
+
head: ObjectHead | null,
|
|
138
|
+
byteSize: number,
|
|
139
|
+
contentType: string,
|
|
140
|
+
sha256: string,
|
|
141
|
+
): void {
|
|
142
|
+
if (
|
|
143
|
+
!head ||
|
|
144
|
+
head.ContentLength !== byteSize ||
|
|
145
|
+
head.ContentType !== contentType ||
|
|
146
|
+
head.Metadata?.sha256 !== sha256 ||
|
|
147
|
+
typeof head.VersionToken !== "string" ||
|
|
148
|
+
head.VersionToken.length < 1
|
|
149
|
+
) {
|
|
150
|
+
throw new Error("Workspace export object differs from its durable materialization");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function exportFilename(value: string, format: string): string {
|
|
155
|
+
const expectedExtension = `.${format}`;
|
|
156
|
+
const normalized = value
|
|
157
|
+
.normalize("NFKC")
|
|
158
|
+
.replace(/[\\/:*?"<>|\u0000-\u001f\u007f]+/gu, "-")
|
|
159
|
+
.replace(/\s+/gu, " ")
|
|
160
|
+
.trim()
|
|
161
|
+
.slice(0, 200);
|
|
162
|
+
const stem = normalized.toLowerCase().endsWith(expectedExtension)
|
|
163
|
+
? normalized.slice(0, -expectedExtension.length)
|
|
164
|
+
: normalized;
|
|
165
|
+
return `${stem.replace(/[. ]+$/u, "").slice(0, 190) || "artifact"}${expectedExtension}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function plainSha256(value: string): string {
|
|
169
|
+
const match = /^sha256:([0-9a-f]{64})$/u.exec(value);
|
|
170
|
+
if (!match) throw new Error("Materialized artifact content hash is invalid");
|
|
171
|
+
return match[1]!;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function deterministicUuid(seed: string): string {
|
|
175
|
+
const bytes = createHash("sha256").update(seed, "utf8").digest().subarray(0, 16);
|
|
176
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x50;
|
|
177
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
178
|
+
const hex = bytes.toString("hex");
|
|
179
|
+
const value = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
180
|
+
if (!UUID_PATTERN.test(value)) throw new Error("Generated workspace file identity is invalid");
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function throwIfAborted(signal?: AbortSignal): void {
|
|
185
|
+
if (signal?.aborted) throw signal.reason ?? new Error("Artifact export aborted");
|
|
186
|
+
}
|
|
@@ -31,13 +31,42 @@ export function githubBrowserGrantClaims(
|
|
|
31
31
|
export function continuedGitHubBrowserGrantClaims(
|
|
32
32
|
payload: GitHubSignedStatePayload,
|
|
33
33
|
): Record<string, unknown> {
|
|
34
|
-
|
|
34
|
+
const browserGrant =
|
|
35
|
+
typeof payload.browserGrantSubjectId === "string" &&
|
|
35
36
|
typeof payload.browserGrantExpiresAt === "number"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
? {
|
|
38
|
+
browserGrantSubjectId: payload.browserGrantSubjectId,
|
|
39
|
+
browserGrantExpiresAt: payload.browserGrantExpiresAt,
|
|
40
|
+
}
|
|
41
|
+
: {};
|
|
42
|
+
return {
|
|
43
|
+
...browserGrant,
|
|
44
|
+
...(typeof payload.returnPath === "string" ? { returnPath: payload.returnPath } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* GitHub may return only to the session that initiated setup. This rejects
|
|
50
|
+
* absolute/protocol-relative URLs and paths for another workspace before the
|
|
51
|
+
* value is signed into provider state.
|
|
52
|
+
*/
|
|
53
|
+
export function githubSessionReturnPath(
|
|
54
|
+
value: string | undefined | null,
|
|
55
|
+
workspaceId: string,
|
|
56
|
+
): string | null {
|
|
57
|
+
if (!value || value.length > 2048 || !value.startsWith("/") || value.startsWith("//")) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const parsed = new URL(value, "https://opengeni.local");
|
|
61
|
+
const sessionPrefix = `/workspaces/${encodeURIComponent(workspaceId)}/sessions/`;
|
|
62
|
+
if (
|
|
63
|
+
parsed.origin !== "https://opengeni.local" ||
|
|
64
|
+
parsed.pathname.startsWith("//") ||
|
|
65
|
+
!parsed.pathname.startsWith(sessionPrefix)
|
|
66
|
+
) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
41
70
|
}
|
|
42
71
|
|
|
43
72
|
export function githubBrowserGrantFromState(
|
package/src/http/auth.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { resolveFirstPartyDelegationSecret, type Settings } from "@opengeni/config";
|
|
2
|
+
import { verifyDelegatedAccessToken } from "@opengeni/contracts";
|
|
2
3
|
import type { Context, MiddlewareHandler } from "hono";
|
|
3
4
|
import { installExactPaths, isInstallRedirectPath } from "../routes/install";
|
|
4
5
|
|
|
@@ -17,7 +18,7 @@ export function requireAccessKey(settings: Settings): MiddlewareHandler {
|
|
|
17
18
|
await next();
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
20
|
-
if (isAuthorized(c, settings
|
|
21
|
+
if (await isAuthorized(c, settings)) {
|
|
21
22
|
await next();
|
|
22
23
|
return;
|
|
23
24
|
}
|
|
@@ -49,11 +50,16 @@ function isAuthExempt(c: Context, settings: Settings): boolean {
|
|
|
49
50
|
}
|
|
50
51
|
if (
|
|
51
52
|
path === "/v1/integrations/oauth/callback" ||
|
|
53
|
+
path === "/v1/integrations/provider-oauth/callback" ||
|
|
54
|
+
path === "/v1/integrations/google-drive/callback" ||
|
|
52
55
|
path === "/v1/integrations/oauth/client-metadata.json" ||
|
|
53
56
|
path === "/v1/integrations/slack/callback" ||
|
|
54
57
|
path === "/v1/integrations/slack/events" ||
|
|
55
58
|
path === "/v1/integrations/slack/commands" ||
|
|
56
|
-
path === "/v1/integrations/slack/interactions"
|
|
59
|
+
path === "/v1/integrations/slack/interactions" ||
|
|
60
|
+
// Fiken OAuth browser redirect: exact path only, protected by signed
|
|
61
|
+
// single-use state plus a callback-time grant recheck.
|
|
62
|
+
path === "/v1/integrations/fiken/callback"
|
|
57
63
|
) {
|
|
58
64
|
return true;
|
|
59
65
|
}
|
|
@@ -98,19 +104,33 @@ function isAuthExempt(c: Context, settings: Settings): boolean {
|
|
|
98
104
|
return false;
|
|
99
105
|
}
|
|
100
106
|
|
|
101
|
-
function isAuthorized(c: Context,
|
|
102
|
-
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
107
|
+
async function isAuthorized(c: Context, settings: Settings): Promise<boolean> {
|
|
108
|
+
const expected = settings.accessKey;
|
|
105
109
|
const explicit = c.req.header("x-opengeni-access-key");
|
|
106
|
-
if (constantTimeEqual(explicit, expected)) {
|
|
110
|
+
if (expected && constantTimeEqual(explicit, expected)) {
|
|
107
111
|
return true;
|
|
108
112
|
}
|
|
109
113
|
const authorization = c.req.header("authorization");
|
|
110
114
|
const bearer = authorization?.startsWith("Bearer ")
|
|
111
115
|
? authorization.slice("Bearer ".length)
|
|
112
116
|
: undefined;
|
|
113
|
-
|
|
117
|
+
if (expected && constantTimeEqual(bearer, expected)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// This middleware is only the deployment perimeter. A valid first-party
|
|
122
|
+
// delegated bearer is already authenticated with the deployment's separate
|
|
123
|
+
// delegation authority, so admit it only to the product API's route-level
|
|
124
|
+
// access resolver. Deployment-only surfaces such as /metrics still require
|
|
125
|
+
// the static deployment key. The resolver enforces exact account, workspace,
|
|
126
|
+
// principal, and permissions; attempt-bound surfaces add live-attempt fences.
|
|
127
|
+
const delegationSecret = resolveFirstPartyDelegationSecret(settings);
|
|
128
|
+
return Boolean(
|
|
129
|
+
c.req.path.startsWith("/v1/") &&
|
|
130
|
+
bearer &&
|
|
131
|
+
delegationSecret &&
|
|
132
|
+
(await verifyDelegatedAccessToken(delegationSecret, bearer)),
|
|
133
|
+
);
|
|
114
134
|
}
|
|
115
135
|
|
|
116
136
|
function constantTimeEqual(actual: string | undefined, expected: string): boolean {
|
package/src/http/cors.ts
ADDED