@pyric/cli 0.1.0-alpha.30 → 0.1.0-alpha.30.2
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/assurance/.generated/conformance-verdicts.d.ts +8 -0
- package/dist/assurance/.generated/conformance-verdicts.d.ts.map +1 -1
- package/dist/assurance/.generated/conformance-verdicts.js +8 -0
- package/dist/assurance/.generated/conformance-verdicts.js.map +1 -1
- package/dist/bridge/server/peer.d.ts +10 -0
- package/dist/bridge/server/peer.d.ts.map +1 -1
- package/dist/bridge/server/peer.js +46 -1
- package/dist/bridge/server/peer.js.map +1 -1
- package/dist/cli/serve.d.ts +2 -0
- package/dist/cli/serve.d.ts.map +1 -1
- package/dist/cli/serve.js +13 -2
- package/dist/cli/serve.js.map +1 -1
- package/dist/conformance/.generated/can-i-use-browser.js +2 -2
- package/dist/conformance/.generated/can-i-use-browser.js.map +1 -1
- package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use.js +1 -1
- package/dist/conformance/.generated/can-i-use.js.map +1 -1
- package/dist/conformance/.generated/conformance-docs.d.ts.map +1 -1
- package/dist/conformance/.generated/conformance-docs.js +1 -1
- package/dist/conformance/.generated/conformance-docs.js.map +1 -1
- package/dist/register/app-bridge-admin-app-check.d.ts +3 -0
- package/dist/register/app-bridge-admin-app-check.d.ts.map +1 -0
- package/dist/register/app-bridge-admin-app-check.js +43 -0
- package/dist/register/app-bridge-admin-app-check.js.map +1 -0
- package/dist/register/app-bridge-app-check.d.ts +4 -0
- package/dist/register/app-bridge-app-check.d.ts.map +1 -1
- package/dist/register/app-bridge-app-check.js +4 -0
- package/dist/register/app-bridge-app-check.js.map +1 -1
- package/dist/register/app-bridge-shadow.d.ts +4 -2
- package/dist/register/app-bridge-shadow.d.ts.map +1 -1
- package/dist/register/app-bridge-shadow.js +29 -5
- package/dist/register/app-bridge-shadow.js.map +1 -1
- package/dist/register/index.d.ts.map +1 -1
- package/dist/register/index.js +21 -0
- package/dist/register/index.js.map +1 -1
- package/dist/serve/ai-diagnostics-relay.d.ts +4 -1
- package/dist/serve/ai-diagnostics-relay.d.ts.map +1 -1
- package/dist/serve/ai-diagnostics-relay.js +3 -0
- package/dist/serve/ai-diagnostics-relay.js.map +1 -1
- package/dist/serve/ai-proxy.d.ts +21 -1
- package/dist/serve/ai-proxy.d.ts.map +1 -1
- package/dist/serve/ai-proxy.js +39 -14
- package/dist/serve/ai-proxy.js.map +1 -1
- package/dist/serve/ai-terminal-blocks.d.ts +6 -4
- package/dist/serve/ai-terminal-blocks.d.ts.map +1 -1
- package/dist/serve/ai-terminal-blocks.js +7 -4
- package/dist/serve/ai-terminal-blocks.js.map +1 -1
- package/dist/serve/ai-terminal-text.d.ts +8 -0
- package/dist/serve/ai-terminal-text.d.ts.map +1 -1
- package/dist/serve/ai-terminal-text.js +10 -0
- package/dist/serve/ai-terminal-text.js.map +1 -1
- package/dist/serve/bundler.d.ts +40 -0
- package/dist/serve/bundler.d.ts.map +1 -1
- package/dist/serve/bundler.js +133 -2
- package/dist/serve/bundler.js.map +1 -1
- package/dist/serve/entries/ai.d.ts.map +1 -1
- package/dist/serve/entries/ai.js +28 -26
- package/dist/serve/entries/ai.js.map +1 -1
- package/dist/serve/entries/app-ai-passthrough.d.ts +4 -4
- package/dist/serve/entries/app-ai-passthrough.d.ts.map +1 -1
- package/dist/serve/entries/app-ai-passthrough.js +8 -52
- package/dist/serve/entries/app-ai-passthrough.js.map +1 -1
- package/dist/serve/firebase-module-swap.d.ts +22 -0
- package/dist/serve/firebase-module-swap.d.ts.map +1 -1
- package/dist/serve/firebase-module-swap.js +51 -3
- package/dist/serve/firebase-module-swap.js.map +1 -1
- package/dist/serve/gemini-error-redaction.d.ts +6 -0
- package/dist/serve/gemini-error-redaction.d.ts.map +1 -0
- package/dist/serve/gemini-error-redaction.js +61 -0
- package/dist/serve/gemini-error-redaction.js.map +1 -0
- package/dist/serve/gemini-proxy.d.ts +40 -0
- package/dist/serve/gemini-proxy.d.ts.map +1 -0
- package/dist/serve/gemini-proxy.js +105 -0
- package/dist/serve/gemini-proxy.js.map +1 -0
- package/dist/serve/hosted/method-protocol.d.ts +4 -4
- package/dist/serve/html-injection.d.ts +2 -0
- package/dist/serve/html-injection.d.ts.map +1 -1
- package/dist/serve/html-injection.js +4 -2
- package/dist/serve/html-injection.js.map +1 -1
- package/dist/serve/namespace.d.ts +9 -0
- package/dist/serve/namespace.d.ts.map +1 -1
- package/dist/serve/namespace.js +71 -15
- package/dist/serve/namespace.js.map +1 -1
- package/dist/serve/sandbox-session.d.ts +3 -0
- package/dist/serve/sandbox-session.d.ts.map +1 -1
- package/dist/serve/sandbox-session.js +1 -0
- package/dist/serve/sandbox-session.js.map +1 -1
- package/dist/serve/site-ui/docs/ai-compat/index.html +29 -10
- package/dist/serve/site-ui/docs/ai-compat.md +30 -9
- package/dist/serve/site-ui/docs/app-check-compat/index.html +42 -18
- package/dist/serve/site-ui/docs/app-check-compat.md +42 -18
- package/dist/serve/site-ui/docs/auth-compat/index.html +4 -4
- package/dist/serve/site-ui/docs/auth-compat.md +4 -4
- package/dist/serve/site-ui/docs/build/ai-logic/index.html +21 -5
- package/dist/serve/site-ui/docs/build/ai-logic.md +23 -2
- package/dist/serve/site-ui/docs/database-compat/index.html +4 -4
- package/dist/serve/site-ui/docs/database-compat.md +4 -4
- package/dist/serve/site-ui/docs/firestore-compat/index.html +4 -4
- package/dist/serve/site-ui/docs/firestore-compat.md +4 -4
- package/dist/serve/site-ui/docs/index.json +10 -0
- package/dist/serve/site-ui/docs/messaging-compat/index.html +4 -4
- package/dist/serve/site-ui/docs/messaging-compat.md +4 -4
- package/dist/serve/site-ui/docs/pyric-admin-app-reference-api/index.html +4 -0
- package/dist/serve/site-ui/docs/pyric-admin-app-reference-api.md +5 -0
- package/dist/serve/site-ui/docs/pyric-cli-register-app-bridge-app-check-reference-api/index.html +2 -0
- package/dist/serve/site-ui/docs/pyric-cli-register-app-bridge-app-check-reference-api.md +3 -0
- package/dist/serve/site-ui/docs/storage-compat/index.html +5 -5
- package/dist/serve/site-ui/docs/storage-compat.md +5 -5
- package/dist/serve/site-ui/docs/tutorial/index.html +11 -1
- package/dist/serve/site-ui/docs/tutorial.md +12 -1
- package/dist/serve/site-ui/examples/chess/index.html +1 -1
- package/dist/serve/vite-sandbox-generation.d.ts.map +1 -1
- package/dist/serve/vite-sandbox-generation.js +2 -0
- package/dist/serve/vite-sandbox-generation.js.map +1 -1
- package/dist/serve/worker/ai-proxy-paths.d.ts +32 -0
- package/dist/serve/worker/ai-proxy-paths.d.ts.map +1 -0
- package/dist/serve/worker/ai-proxy-paths.js +20 -0
- package/dist/serve/worker/ai-proxy-paths.js.map +1 -0
- package/dist/serve/worker/client/ai.d.ts +3 -1
- package/dist/serve/worker/client/ai.d.ts.map +1 -1
- package/dist/serve/worker/client/ai.js +3 -0
- package/dist/serve/worker/client/ai.js.map +1 -1
- package/dist/serve/worker/entry.js +2 -0
- package/dist/serve/worker/entry.js.map +1 -1
- package/dist/serve/worker/host-ai.d.ts +4 -5
- package/dist/serve/worker/host-ai.d.ts.map +1 -1
- package/dist/serve/worker/host-ai.js +19 -23
- package/dist/serve/worker/host-ai.js.map +1 -1
- package/dist/serve/worker/host-context.d.ts +13 -4
- package/dist/serve/worker/host-context.d.ts.map +1 -1
- package/dist/serve/worker/host-context.js.map +1 -1
- package/dist/serve/worker/inbound-validation/operation-arguments.d.ts.map +1 -1
- package/dist/serve/worker/inbound-validation/operation-arguments.js +11 -0
- package/dist/serve/worker/inbound-validation/operation-arguments.js.map +1 -1
- package/dist/serve/worker/protocol.d.ts +13 -0
- package/dist/serve/worker/protocol.d.ts.map +1 -1
- package/dist/serve/worker/protocol.js.map +1 -1
- package/package.json +4 -4
- package/src/assurance/.generated/conformance-verdicts.ts +8 -0
- package/src/bridge/server/peer.ts +49 -1
- package/src/cli/serve.ts +14 -2
- package/src/conformance/.generated/can-i-use-browser.ts +2 -2
- package/src/conformance/.generated/can-i-use.ts +1 -1
- package/src/conformance/.generated/conformance-docs.ts +1 -1
- package/src/register/app-bridge-admin-app-check.ts +46 -0
- package/src/register/app-bridge-app-check.ts +4 -0
- package/src/register/app-bridge-shadow.ts +35 -5
- package/src/register/index.ts +23 -0
- package/src/serve/ai-diagnostics-relay.ts +6 -1
- package/src/serve/ai-proxy.ts +43 -14
- package/src/serve/ai-terminal-blocks.ts +8 -4
- package/src/serve/ai-terminal-text.ts +11 -0
- package/src/serve/bundler.ts +163 -2
- package/src/serve/entries/ai.ts +28 -27
- package/src/serve/entries/app-ai-passthrough.ts +8 -58
- package/src/serve/firebase-module-swap.ts +52 -3
- package/src/serve/gemini-error-redaction.ts +60 -0
- package/src/serve/gemini-proxy.ts +140 -0
- package/src/serve/html-injection.ts +4 -2
- package/src/serve/namespace.ts +74 -15
- package/src/serve/sandbox-session.ts +4 -0
- package/src/serve/vite-sandbox-generation.ts +2 -0
- package/src/serve/worker/ai-proxy-paths.ts +35 -0
- package/src/serve/worker/client/ai.ts +6 -1
- package/src/serve/worker/entry.ts +2 -0
- package/src/serve/worker/host-ai.ts +23 -23
- package/src/serve/worker/host-context.ts +13 -3
- package/src/serve/worker/inbound-validation/operation-arguments.ts +11 -0
- package/src/serve/worker/protocol.ts +14 -2
package/src/serve/namespace.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { createReadStream, existsSync, statSync } from 'node:fs';
|
|
|
23
23
|
import { basename, join } from 'node:path';
|
|
24
24
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
25
25
|
import type { ActivityIncident } from 'pyric/firestore/internal';
|
|
26
|
-
import { collectBody } from '../bridge/server/peer.js';
|
|
26
|
+
import { BODY_TOO_LARGE_CODE, collectBody } from '../bridge/server/peer.js';
|
|
27
27
|
import { handleIndexConfig } from './index-config-route.js';
|
|
28
28
|
import { StateFileError, type StateSection, type StateStore } from './state-store.js';
|
|
29
29
|
import { createWriterLock, type WriterLock } from './writer-lock.js';
|
|
@@ -32,7 +32,9 @@ import { pipeFileToResponse, getHeader, isAllowedHost, isAllowedOrigin, isAllowe
|
|
|
32
32
|
import type { InitPayload } from './init-payload.js';
|
|
33
33
|
import { handleActivity } from './activity-route.js';
|
|
34
34
|
import { handleAiProxy, AI_PROXY_ROUTE } from './ai-proxy.js';
|
|
35
|
+
import { createGeminiProxy, isGeminiProxyPath, type GeminiProxyConfig } from './gemini-proxy.js';
|
|
35
36
|
import { aiTerminalBlockFor, type AiDiagnosticPayload } from './ai-terminal-blocks.js';
|
|
37
|
+
import { sanitizeForTerminal, stripTerminalControls } from './ai-terminal-text.js';
|
|
36
38
|
import { createSiteTreeHandler } from './site-tree.js';
|
|
37
39
|
import { handleAvatar } from './assets/avatar-route.js';
|
|
38
40
|
import { ASSETS_ROUTE_PREFIX } from './assets/avatar-url.js';
|
|
@@ -147,6 +149,10 @@ export interface NamespaceOptions {
|
|
|
147
149
|
* (local Ollama). Always mounted — the route only touches the network
|
|
148
150
|
* when a request arrives. */
|
|
149
151
|
aiProxyUpstream?: string;
|
|
152
|
+
/** The gemini engine `/__pyric/ai-proxy/gemini` sends for a browser engine
|
|
153
|
+
* (#1584): the Vite plugin's `ai.engine` when it is gemini. Absent, it
|
|
154
|
+
* sends to Google AI Studio with the key from this process's environment. */
|
|
155
|
+
aiGemini?: GeminiProxyConfig;
|
|
150
156
|
/** Receives one handshake beacon per pyric-launched child (`POST
|
|
151
157
|
* /__pyric/beacon`, sent by `@pyric/cli/register` once its hooks and
|
|
152
158
|
* net-guard are installed). Absent means the route still 204s; the child
|
|
@@ -354,6 +360,11 @@ async function handleCapture(
|
|
|
354
360
|
|
|
355
361
|
const DENIAL_THROTTLE_MS = 5_000;
|
|
356
362
|
|
|
363
|
+
/** Largest relayed denial the route reads. A real one is a message, a path, a
|
|
364
|
+
* uid and a short owner stack, a few KiB at most; the index route's 64 KiB
|
|
365
|
+
* leaves room for a deep stack without letting a page grow the server. */
|
|
366
|
+
export const DENIAL_BODY_LIMIT_BYTES = 64 * 1024;
|
|
367
|
+
|
|
357
368
|
/** Loosely-typed mirror of the client's relay payload — JSON from the served
|
|
358
369
|
* page, read defensively since nothing here enforces the wire shape. */
|
|
359
370
|
interface DenialRelayPayload {
|
|
@@ -397,27 +408,48 @@ export function createDenialThrottle(windowMs: number = DENIAL_THROTTLE_MS): {
|
|
|
397
408
|
}
|
|
398
409
|
export type DenialThrottle = ReturnType<typeof createDenialThrottle>;
|
|
399
410
|
|
|
411
|
+
/** A page-supplied string as one terminal-safe line. Every field of a relayed
|
|
412
|
+
* denial comes from the served page, which need not be the developer's own
|
|
413
|
+
* code, so none of it reaches the terminal with control characters intact. */
|
|
414
|
+
function pageText(value: string): string {
|
|
415
|
+
return sanitizeForTerminal(value);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** A multi-line page-supplied string (a query-proof remediation) as indented
|
|
419
|
+
* terminal lines. Each line keeps its spacing and its whole length, because a
|
|
420
|
+
* reader pastes the `pyric rules test` command and the `Fixture case:` JSON
|
|
421
|
+
* out of it; only control and bidi characters are replaced. The denial body
|
|
422
|
+
* limit bounds the total, so no line or line count is cut here. */
|
|
423
|
+
function pageLines(value: string): string[] {
|
|
424
|
+
const lines = value.split(/\r?\n/).map((line) => stripTerminalControls(line).trimEnd());
|
|
425
|
+
const first = lines.findIndex((line) => line.length > 0);
|
|
426
|
+
if (first === -1) return [];
|
|
427
|
+
let last = lines.length - 1;
|
|
428
|
+
while (lines[last]!.length === 0) last -= 1;
|
|
429
|
+
return lines.slice(first, last + 1).map((line) => (line.length > 0 ? ` ${line}` : ''));
|
|
430
|
+
}
|
|
431
|
+
|
|
400
432
|
/** Format a relayed denial into the compact terminal block: the message,
|
|
401
433
|
* then (when present) the request method/path, the auth uid, and any
|
|
402
434
|
* remediation guidance. A few lines, never a JSON dump. */
|
|
403
435
|
export function formatDenialBlock(payload: DenialRelayPayload): string {
|
|
404
436
|
const message = typeof payload.message === 'string' ? payload.message : 'permission denied';
|
|
405
|
-
const lines = [` ⚠ [pyric] denied: ${message}`];
|
|
437
|
+
const lines = [` ⚠ [pyric] denied: ${pageText(message)}`];
|
|
406
438
|
const ctx = payload.denialContext;
|
|
407
439
|
const request = ctx?.request;
|
|
408
440
|
if (request?.method && request?.path) {
|
|
409
|
-
const instance = typeof payload.instance === 'string' ? ` (${payload.instance})` : '';
|
|
410
|
-
lines.push(` ${request.method} ${request.path}${instance}`);
|
|
441
|
+
const instance = typeof payload.instance === 'string' ? ` (${pageText(payload.instance)})` : '';
|
|
442
|
+
lines.push(` ${pageText(String(request.method))} ${pageText(String(request.path))}${instance}`);
|
|
411
443
|
}
|
|
412
444
|
const uid = ctx?.auth?.uid;
|
|
413
|
-
lines.push(` auth: ${uid
|
|
445
|
+
lines.push(` auth: ${uid === undefined || uid === null ? 'anonymous' : pageText(String(uid))}`);
|
|
414
446
|
// An RTDB message names no rule, so the block names the deciding one.
|
|
415
447
|
if (ctx?.engine === 'rtdb' && typeof ctx.matchedRule === 'string') {
|
|
416
|
-
const node = typeof ctx.matchedPath === 'string' ? `${ctx.matchedPath} ` : '';
|
|
417
|
-
lines.push(` rule: ${node}${ctx.matchedRule}`);
|
|
448
|
+
const node = typeof ctx.matchedPath === 'string' ? `${pageText(ctx.matchedPath)} ` : '';
|
|
449
|
+
lines.push(` rule: ${node}${pageText(ctx.matchedRule)}`);
|
|
418
450
|
}
|
|
419
451
|
const remediation = typeof payload.remediation === 'string' ? payload.remediation : ctx?.remediation;
|
|
420
|
-
if (remediation) lines.push(
|
|
452
|
+
if (remediation) lines.push(...pageLines(String(remediation)));
|
|
421
453
|
lines.push(...ownerLines(payload.owners));
|
|
422
454
|
return lines.join('\n');
|
|
423
455
|
}
|
|
@@ -437,7 +469,7 @@ function isRelayedFrame(value: unknown): value is RelayedFrame {
|
|
|
437
469
|
/** A frame's file as a reader looks for it: a served URL loses its origin and query. */
|
|
438
470
|
function frameLocation(frame: RelayedFrame): string {
|
|
439
471
|
const path = frame.file.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]*/i, '').replace(/[?#].*$/, '');
|
|
440
|
-
return `${path}:${frame.line}
|
|
472
|
+
return pageText(`${path}:${frame.line}`);
|
|
441
473
|
}
|
|
442
474
|
|
|
443
475
|
/**
|
|
@@ -450,7 +482,7 @@ function ownerLines(owners: unknown): string[] {
|
|
|
450
482
|
if (!Array.isArray(owners)) return [];
|
|
451
483
|
const frame = owners.find((owner) => (owner as { kind?: unknown })?.kind === 'frame');
|
|
452
484
|
if (!isRelayedFrame(frame)) return [];
|
|
453
|
-
const name = typeof frame.function === 'string' && frame.function.length > 0 ? `${frame.function} ` : '';
|
|
485
|
+
const name = typeof frame.function === 'string' && frame.function.length > 0 ? `${pageText(frame.function)} ` : '';
|
|
454
486
|
const lines = [` owner: ${name}${frameLocation(frame)}`];
|
|
455
487
|
const stack = (frame as { stack?: unknown }).stack;
|
|
456
488
|
const chain = Array.isArray(stack) ? stack.filter(isRelayedFrame) : [];
|
|
@@ -492,8 +524,12 @@ export function noteAiDiagnostic(
|
|
|
492
524
|
* (target, reason) per window is the right volume for every one of them. A
|
|
493
525
|
* rules denial's target also names its RTDB instance, so the same path
|
|
494
526
|
* denied on two instances prints once for each.
|
|
495
|
-
*
|
|
496
|
-
* logger wired must never fail the page that reported the refusal)
|
|
527
|
+
* Answers 204 (best-effort diagnostics: a malformed body or a caller with no
|
|
528
|
+
* logger wired must never fail the page that reported the refusal), except
|
|
529
|
+
* 413 for a body over {@link DENIAL_BODY_LIMIT_BYTES}. On the 0.1.0-alpha.30
|
|
530
|
+
* line the namespace runs no loopback guard before this route, so a page on
|
|
531
|
+
* any origin can reach here; the body limit and the terminal-text sanitizing
|
|
532
|
+
* still apply. */
|
|
497
533
|
async function handleDenials(
|
|
498
534
|
throttle: DenialThrottle,
|
|
499
535
|
logger: ServeLogger | undefined,
|
|
@@ -505,7 +541,7 @@ async function handleDenials(
|
|
|
505
541
|
return;
|
|
506
542
|
}
|
|
507
543
|
try {
|
|
508
|
-
const payload = (await collectBody(req)) as DenialRelayPayload & AiDiagnosticPayload;
|
|
544
|
+
const payload = (await collectBody(req, DENIAL_BODY_LIMIT_BYTES)) as DenialRelayPayload & AiDiagnosticPayload;
|
|
509
545
|
const isAiDiagnostic = aiTerminalBlockFor(payload.kind) !== null;
|
|
510
546
|
if (isAiDiagnostic) {
|
|
511
547
|
noteAiDiagnostic(throttle, logger, payload);
|
|
@@ -519,7 +555,12 @@ async function handleDenials(
|
|
|
519
555
|
logger.note(formatDenialBlock(payload));
|
|
520
556
|
}
|
|
521
557
|
}
|
|
522
|
-
} catch {
|
|
558
|
+
} catch (error) {
|
|
559
|
+
const tooLarge = (error as { code?: unknown } | null)?.code === BODY_TOO_LARGE_CODE;
|
|
560
|
+
if (tooLarge) {
|
|
561
|
+
res.writeHead(413, { 'content-type': 'text/plain' }).end('Payload Too Large');
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
523
564
|
/* malformed body: drop it; this is a diagnostics side channel */
|
|
524
565
|
}
|
|
525
566
|
res.writeHead(204).end();
|
|
@@ -573,6 +614,7 @@ export function createPyricNamespace(opts: NamespaceOptions) {
|
|
|
573
614
|
? createSiteTreeHandler(siteUiDir, opts.workerVersion, hostedStudioProject(opts))
|
|
574
615
|
: null;
|
|
575
616
|
const denialThrottle = createDenialThrottle();
|
|
617
|
+
const geminiProxy = createGeminiProxy(opts.aiGemini, denialThrottle, opts.logger);
|
|
576
618
|
// Issued once per server boot. The outer static/Vite host guard protects
|
|
577
619
|
// init.json before this capability is disclosed to the served runtime.
|
|
578
620
|
const activity = opts.activity;
|
|
@@ -688,8 +730,18 @@ export function createPyricNamespace(opts: NamespaceOptions) {
|
|
|
688
730
|
events.handle(req, res);
|
|
689
731
|
return true;
|
|
690
732
|
}
|
|
733
|
+
const isGeminiProxyRequest = isGeminiProxyPath(url.pathname);
|
|
734
|
+
if (isGeminiProxyRequest) {
|
|
735
|
+
// It spends the server's key, so only this app's pages may call it.
|
|
736
|
+
const hostRefused = !guardLoopback(req, res, opts.boundHost ?? 'localhost', opts.allowedHosts);
|
|
737
|
+
if (hostRefused) return true;
|
|
738
|
+
return geminiProxy(req, res, url).then(() => true);
|
|
739
|
+
}
|
|
691
740
|
const isAiProxyRequest = url.pathname === AI_PROXY_ROUTE || url.pathname.startsWith(`${AI_PROXY_ROUTE}/`);
|
|
692
741
|
if (isAiProxyRequest) {
|
|
742
|
+
// The upstream may carry the developer's key, so only this server's pages may spend it.
|
|
743
|
+
const hostRefused = !guardLoopback(req, res, opts.boundHost ?? 'localhost', opts.allowedHosts);
|
|
744
|
+
if (hostRefused) return true;
|
|
693
745
|
return handleAiProxy(opts.aiProxyUpstream, denialThrottle, req, res, url, opts.logger).then(
|
|
694
746
|
() => true,
|
|
695
747
|
);
|
|
@@ -700,6 +752,9 @@ export function createPyricNamespace(opts: NamespaceOptions) {
|
|
|
700
752
|
}
|
|
701
753
|
const isDenialRequest = url.pathname === '/__pyric/denials';
|
|
702
754
|
if (isDenialRequest) {
|
|
755
|
+
// No origin guard on the 0.1.0-alpha.30 line: without the learned proxy
|
|
756
|
+
// ports from #1497, it would refuse the Next dev server's page under
|
|
757
|
+
// `pyric sandbox -- next dev`. The body limit still applies.
|
|
703
758
|
return handleDenials(denialThrottle, opts.logger, req, res).then(() => true);
|
|
704
759
|
}
|
|
705
760
|
const isInitRequest = url.pathname === '/__pyric/init.json';
|
|
@@ -731,7 +786,11 @@ export function createPyricNamespace(opts: NamespaceOptions) {
|
|
|
731
786
|
const file = join(opts.sdkDir, basename(url.pathname));
|
|
732
787
|
const missingFile = !existsSync(file);
|
|
733
788
|
if (missingFile) {
|
|
734
|
-
|
|
789
|
+
const isRealSdk = basename(url.pathname).startsWith('firebase-sdk-');
|
|
790
|
+
const body = isRealSdk
|
|
791
|
+
? 'pyric sandbox: the Firebase SDK was not built. Install firebase where pyric starts (production AI mode).'
|
|
792
|
+
: 'not found';
|
|
793
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' }).end(body);
|
|
735
794
|
return true;
|
|
736
795
|
}
|
|
737
796
|
const isSourceMap = file.endsWith('.map');
|
|
@@ -44,6 +44,7 @@ import { restoredStateCounts, stateForSummary } from './state-summary.js';
|
|
|
44
44
|
import { parseStateFile } from './state-file.js';
|
|
45
45
|
import { inlineStorage } from './state-objects.js';
|
|
46
46
|
import { createHostedPersistence, formatStorageRepairs, type HostedPersistence } from './hosted/persistence.js';
|
|
47
|
+
import type { GeminiProxyConfig } from './gemini-proxy.js';
|
|
47
48
|
import {
|
|
48
49
|
runProjectFixturesOnRulesReload,
|
|
49
50
|
type RulesFixtureCheckSummary,
|
|
@@ -69,6 +70,8 @@ export interface SandboxSessionOptions {
|
|
|
69
70
|
deployHostedRules?: (service: 'firestore' | 'database' | 'storage', source: string | null, instance?: DatabaseRulesDestination) => Promise<void>;
|
|
70
71
|
ai?: InitPayload['ai'];
|
|
71
72
|
aiProxyUpstream?: string;
|
|
73
|
+
/** The gemini engine the dev server's gemini proxy sends for a browser engine. */
|
|
74
|
+
aiGemini?: GeminiProxyConfig;
|
|
72
75
|
/** Resolved `avatars` option (already reduced by `avatars-config.ts` from
|
|
73
76
|
* whatever the caller — the Vite plugin or `pyric sandbox` — accepted).
|
|
74
77
|
* Absent behaves like `{ enabled: false }`: no avatar route is mounted. */
|
|
@@ -444,6 +447,7 @@ export async function createSandboxSession(
|
|
|
444
447
|
workerVersion: options.sdk.workerVersion,
|
|
445
448
|
avatars: avatarsResolver,
|
|
446
449
|
aiProxyUpstream: options.aiProxyUpstream,
|
|
450
|
+
aiGemini: options.aiGemini,
|
|
447
451
|
activity: options.activity,
|
|
448
452
|
beacon: options.beacon,
|
|
449
453
|
beaconToken: options.beaconToken,
|
|
@@ -33,6 +33,7 @@ import { resolveAvatarsConfig, type PyricAvatarsOptions } from './avatars-config
|
|
|
33
33
|
import { resolveViteRulesConfig } from './vite-rules-source.js';
|
|
34
34
|
import type { ViteWorkerRuntime, ViteWorkerRuntimeStatus } from './vite-worker-runtime.js';
|
|
35
35
|
import { viteWorkerEpochSalt } from './vite-ai-config.js';
|
|
36
|
+
import { geminiProxyConfig } from './gemini-proxy.js';
|
|
36
37
|
import {
|
|
37
38
|
attachViteGenerationBridge,
|
|
38
39
|
createViteGenerationBridge,
|
|
@@ -278,6 +279,7 @@ export async function createViteSandboxGeneration(
|
|
|
278
279
|
ai: aiOptions,
|
|
279
280
|
avatars: avatarsConfig,
|
|
280
281
|
aiProxyUpstream: ai.proxyUpstream,
|
|
282
|
+
aiGemini: geminiProxyConfig(ai.engineWire),
|
|
281
283
|
activity: (incident) => server.config.logger.warn(formatActivityWarning(incident)),
|
|
282
284
|
logger: {
|
|
283
285
|
info: (message) => server.config.logger.info(message),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dev server's AI proxy routes, as the browser engines name them, and
|
|
3
|
+
* the gemini engine a browser page or SharedWorker runs.
|
|
4
|
+
*
|
|
5
|
+
* Browser-safe: the worker host and the served `firebase/ai` entry import
|
|
6
|
+
* this, so it imports nothing from node.
|
|
7
|
+
*/
|
|
8
|
+
import type { AiEngineConfigWire } from './protocol.js';
|
|
9
|
+
|
|
10
|
+
/** The serve proxy route the browser openai engine defaults to (#98.2). */
|
|
11
|
+
export const AI_PROXY_PATH = '/__pyric/ai-proxy';
|
|
12
|
+
|
|
13
|
+
/** The serve route a browser gemini engine sends through. The server sends
|
|
14
|
+
* each request on to Gemini with the key from its own environment (#1584). */
|
|
15
|
+
export const GEMINI_PROXY_PATH = `${AI_PROXY_PATH}/gemini`;
|
|
16
|
+
|
|
17
|
+
type GeminiWire = Extract<AiEngineConfigWire, { kind: 'gemini' }>;
|
|
18
|
+
|
|
19
|
+
/** A browser gemini engine config: the wire shape, plus the flag that tells
|
|
20
|
+
* the engine the dev server attaches the credentials. */
|
|
21
|
+
export type BrowserGeminiEngine = GeminiWire & { serverAttachesAuth?: true };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The gemini engine config for a browser. The browser has no server
|
|
25
|
+
* environment, so a config without its own `apiKey` sends through the dev
|
|
26
|
+
* server's {@link GEMINI_PROXY_PATH}, which attaches the key and sends to
|
|
27
|
+
* the server's configured base URL: the key never reaches the browser. A
|
|
28
|
+
* config with an `apiKey` already holds its key in the browser, so it keeps
|
|
29
|
+
* its own target.
|
|
30
|
+
*/
|
|
31
|
+
export function browserGeminiEngine(wire: GeminiWire): BrowserGeminiEngine {
|
|
32
|
+
const holdsItsOwnKey = wire.apiKey !== undefined;
|
|
33
|
+
if (holdsItsOwnKey) return { ...wire };
|
|
34
|
+
return { kind: 'gemini', baseUrl: GEMINI_PROXY_PATH, serverAttachesAuth: true };
|
|
35
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* as the mirror AnswerEngine on the worker path (see entries/ai.ts).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { AiEngineConfigWire, InboundMessage } from '../protocol.js';
|
|
6
|
+
import type { AiAppCheckWire, AiEngineConfigWire, InboundMessage } from '../protocol.js';
|
|
7
7
|
import { closeSubscription, nextId, nextSubId, openSnapshotSubscription, rpc } from './core.js';
|
|
8
8
|
import type { ClientDb } from './handles.js';
|
|
9
9
|
|
|
@@ -26,6 +26,8 @@ export interface AiOpParams {
|
|
|
26
26
|
request: Record<string, unknown>;
|
|
27
27
|
/** First-op engine config — see {@link AiEngineConfigWire}. */
|
|
28
28
|
engine?: AiEngineConfigWire;
|
|
29
|
+
/** The App Check claim an app-backed handle attaches — see {@link AiAppCheckWire}. */
|
|
30
|
+
appCheck?: AiAppCheckWire;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/** Unary `generateContent` against the worker's broker. Resolves with the
|
|
@@ -42,6 +44,7 @@ export async function aiGenerateContent(
|
|
|
42
44
|
model: params.model,
|
|
43
45
|
request: params.request,
|
|
44
46
|
...(params.engine !== undefined ? { engine: params.engine } : {}),
|
|
47
|
+
...(params.appCheck !== undefined ? { appCheck: params.appCheck } : {}),
|
|
45
48
|
})) as Record<string, unknown>;
|
|
46
49
|
}
|
|
47
50
|
|
|
@@ -57,6 +60,7 @@ export async function aiCountTokens(
|
|
|
57
60
|
model: params.model,
|
|
58
61
|
request: params.request,
|
|
59
62
|
...(params.engine !== undefined ? { engine: params.engine } : {}),
|
|
63
|
+
...(params.appCheck !== undefined ? { appCheck: params.appCheck } : {}),
|
|
60
64
|
})) as Record<string, unknown>;
|
|
61
65
|
}
|
|
62
66
|
|
|
@@ -100,6 +104,7 @@ export async function* aiStreamGenerateContent(
|
|
|
100
104
|
model: params.model,
|
|
101
105
|
request: params.request,
|
|
102
106
|
...(params.engine !== undefined ? { engine: params.engine } : {}),
|
|
107
|
+
...(params.appCheck !== undefined ? { appCheck: params.appCheck } : {}),
|
|
103
108
|
} satisfies InboundMessage);
|
|
104
109
|
if (!opened) push({
|
|
105
110
|
kind: 'error',
|
|
@@ -123,6 +123,8 @@ async function buildCtx(): Promise<HostCtx> {
|
|
|
123
123
|
? (url) => new EventSource(url) as unknown as EventSourceLike
|
|
124
124
|
: null,
|
|
125
125
|
});
|
|
126
|
+
// This worker runs in the browser: the gemini engine sends through the dev server.
|
|
127
|
+
ctx.aiRunsInBrowser = true;
|
|
126
128
|
return ctx;
|
|
127
129
|
}
|
|
128
130
|
|
|
@@ -10,7 +10,9 @@ import { packAiEvidence, setAiEvidence, type AiEvidence } from 'pyric/ai/interna
|
|
|
10
10
|
* 2. The openai engine's default base URL in this (browser) context is the
|
|
11
11
|
* same-origin `/__pyric/ai-proxy` route, so a localhost upstream (Ollama)
|
|
12
12
|
* needs no CORS setup. Relative URLs resolve against the worker script's
|
|
13
|
-
* origin, so the plain path works from inside the SharedWorker.
|
|
13
|
+
* origin, so the plain path works from inside the SharedWorker. The
|
|
14
|
+
* gemini engine sends through `/__pyric/ai-proxy/gemini`, where the
|
|
15
|
+
* server attaches the key, so the key never enters the browser.
|
|
14
16
|
* 3. `ai.generateContent` / `ai.countTokens` are unary ops exactly like
|
|
15
17
|
* `rtdb.*` / `auth.*`; `ai.streamGenerateContent` is a FINITE
|
|
16
18
|
* subscription that delivers `{ chunk }` snaps in order, then a terminal
|
|
@@ -31,9 +33,9 @@ import { getAI, TARGET_SYMBOL, type AIOptions } from 'pyric/ai';
|
|
|
31
33
|
|
|
32
34
|
import { serializeError, type OpMessage, type AiStreamSubMessage, type AiEngineConfigWire } from './protocol.js';
|
|
33
35
|
import { type HostCtx, type PortLike, type AiBrokerLike, post, ok, fail } from './host-context.js';
|
|
36
|
+
import { AI_PROXY_PATH, browserGeminiEngine } from './ai-proxy-paths.js';
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
export const AI_PROXY_PATH = '/__pyric/ai-proxy';
|
|
38
|
+
export { AI_PROXY_PATH } from './ai-proxy-paths.js';
|
|
37
39
|
|
|
38
40
|
/** Is this op an AI op (routed here instead of handleOp)? */
|
|
39
41
|
export function isAiOp(method: OpMessage['method']): boolean {
|
|
@@ -50,7 +52,11 @@ type AiOpMessage = Extract<OpMessage, { method: 'ai.generateContent' | 'ai.count
|
|
|
50
52
|
* plain authoring shapes; predicate matchers never made it across the port —
|
|
51
53
|
* structured clone rejects functions loudly at send time).
|
|
52
54
|
*/
|
|
53
|
-
function resolveEngineConfig(
|
|
55
|
+
function resolveEngineConfig(
|
|
56
|
+
wire: AiEngineConfigWire,
|
|
57
|
+
upstream: HostCtx['aiUpstream'],
|
|
58
|
+
runsInBrowser: boolean,
|
|
59
|
+
): NonNullable<AIOptions['engine']> {
|
|
54
60
|
if (wire.kind === 'openai') {
|
|
55
61
|
const configuredUrl = wire.baseUrl ?? AI_PROXY_PATH;
|
|
56
62
|
const usesProxy = configuredUrl.replace(/\/$/, '') === AI_PROXY_PATH;
|
|
@@ -65,18 +71,10 @@ function resolveEngineConfig(wire: AiEngineConfigWire, upstream: HostCtx['aiUpst
|
|
|
65
71
|
}
|
|
66
72
|
const isGeminiWire = wire.kind === 'gemini';
|
|
67
73
|
if (isGeminiWire) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (hasBaseUrl) {
|
|
73
|
-
result.baseUrl = wire.baseUrl;
|
|
74
|
-
}
|
|
75
|
-
const hasApiKey = wire.apiKey !== undefined;
|
|
76
|
-
if (hasApiKey) {
|
|
77
|
-
result.apiKey = wire.apiKey;
|
|
78
|
-
}
|
|
79
|
-
return result as unknown as NonNullable<AIOptions['engine']>;
|
|
74
|
+
// Node reads the key from its own environment. A browser worker has
|
|
75
|
+
// none, so it sends through the dev server's gemini proxy.
|
|
76
|
+
const config = runsInBrowser ? browserGeminiEngine(wire) : wire;
|
|
77
|
+
return { ...config } as NonNullable<AIOptions['engine']>;
|
|
80
78
|
}
|
|
81
79
|
return {
|
|
82
80
|
kind: 'scripted',
|
|
@@ -100,7 +98,7 @@ function resolveEngineConfig(wire: AiEngineConfigWire, upstream: HostCtx['aiUpst
|
|
|
100
98
|
export function ensureAiBroker(ctx: HostCtx, opEngine?: AiEngineConfigWire): AiBrokerLike {
|
|
101
99
|
if (ctx.aiBroker) return ctx.aiBroker;
|
|
102
100
|
const wire = ctx.aiEngine ?? opEngine;
|
|
103
|
-
const ai = getAI(ctx.sandbox, wire ? { engine: resolveEngineConfig(wire, ctx.aiUpstream) } : undefined);
|
|
101
|
+
const ai = getAI(ctx.sandbox, wire ? { engine: resolveEngineConfig(wire, ctx.aiUpstream, ctx.aiRunsInBrowser === true) } : undefined);
|
|
104
102
|
const target = (ai as unknown as {
|
|
105
103
|
[TARGET_SYMBOL]?: { broker: AiBrokerLike };
|
|
106
104
|
})[TARGET_SYMBOL];
|
|
@@ -123,14 +121,14 @@ export async function handleAiOp(ctx: HostCtx, port: PortLike, msg: OpMessage):
|
|
|
123
121
|
observation.identity(identity);
|
|
124
122
|
switch (aiMsg.method) {
|
|
125
123
|
case 'ai.generateContent': {
|
|
126
|
-
const response = await broker.generateContent(aiMsg.request, aiMsg.model);
|
|
124
|
+
const response = await broker.generateContent(aiMsg.request, aiMsg.model, aiMsg.appCheck);
|
|
127
125
|
observation.response(response);
|
|
128
126
|
observation.finish('completed');
|
|
129
127
|
ok(port, aiMsg.id, packAiEvidence(response));
|
|
130
128
|
break;
|
|
131
129
|
}
|
|
132
130
|
case 'ai.countTokens': {
|
|
133
|
-
const response = await broker.countTokens(aiMsg.request, aiMsg.model);
|
|
131
|
+
const response = await broker.countTokens(aiMsg.request, aiMsg.model, aiMsg.appCheck);
|
|
134
132
|
observation.response(response);
|
|
135
133
|
observation.finish('completed');
|
|
136
134
|
ok(port, aiMsg.id, packAiEvidence(response));
|
|
@@ -156,9 +154,9 @@ export async function handleAiOp(ctx: HostCtx, port: PortLike, msg: OpMessage):
|
|
|
156
154
|
* (sub handling is synchronous, like the other sub handlers); a client
|
|
157
155
|
* `unsub` mid-stream flips the registered cancel and delivery stops.
|
|
158
156
|
*
|
|
159
|
-
*
|
|
160
|
-
* iteration) — either way a throw lands as
|
|
161
|
-
* convention, terminal for this sub.
|
|
157
|
+
* The App Check gate runs first, then broker validation runs eagerly inside
|
|
158
|
+
* `streamGenerateContent` (before iteration) — either way a throw lands as
|
|
159
|
+
* the shared `{ __error }` snap convention, terminal for this sub.
|
|
162
160
|
*/
|
|
163
161
|
export function handleAiSub(ctx: HostCtx, port: PortLike, msg: AiStreamSubMessage): void {
|
|
164
162
|
let portSubs = ctx.subs.get(port);
|
|
@@ -182,7 +180,9 @@ export function handleAiSub(ctx: HostCtx, port: PortLike, msg: AiStreamSubMessag
|
|
|
182
180
|
const broker = ensureAiBroker(ctx, msg.engine);
|
|
183
181
|
identity = broker.observationIdentity?.(msg.model);
|
|
184
182
|
observation.identity(identity);
|
|
185
|
-
|
|
183
|
+
// The App Check gate answers before any chunk, as AI Logic answers 403 before a stream.
|
|
184
|
+
await broker.admitAppCheck(msg.model, msg.appCheck);
|
|
185
|
+
for await (const chunk of broker.streamGenerateContent(msg.request, msg.model, msg.appCheck)) {
|
|
186
186
|
if (cancelled) return;
|
|
187
187
|
observation.response(chunk, true);
|
|
188
188
|
post(port, { t: 'snap', subId: msg.subId, value: { chunk: packAiEvidence(chunk) } });
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type OutboundMessage,
|
|
25
25
|
type AuthPersistenceMode,
|
|
26
26
|
type AiEngineConfigWire,
|
|
27
|
+
type AiAppCheckWire,
|
|
27
28
|
type AppConfigMessage,
|
|
28
29
|
} from './protocol.js';
|
|
29
30
|
|
|
@@ -40,9 +41,11 @@ import {
|
|
|
40
41
|
*/
|
|
41
42
|
export interface AiBrokerLike {
|
|
42
43
|
observationIdentity?(model: string): import('pyric/ai/internal').AiEvidence;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
/** AI Logic's App Check gate; a request with no claim passes. */
|
|
45
|
+
admitAppCheck(model: string, appCheck: AiAppCheckWire | undefined): Promise<void>;
|
|
46
|
+
generateContent(req: Record<string, unknown>, model: string, appCheck?: AiAppCheckWire): Promise<Record<string, unknown>>;
|
|
47
|
+
streamGenerateContent(req: Record<string, unknown>, model: string, appCheck?: AiAppCheckWire): AsyncIterable<Record<string, unknown>>;
|
|
48
|
+
countTokens(req: Record<string, unknown>, model: string, appCheck?: AiAppCheckWire): Promise<Record<string, unknown>>;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
// ─── Port interface ───────────────────────────────────────────────────────
|
|
@@ -278,6 +281,13 @@ export interface HostCtx {
|
|
|
278
281
|
aiEngine?: AiEngineConfigWire;
|
|
279
282
|
/** Server-only OpenAI transport. Browser workers retain the same-origin proxy. */
|
|
280
283
|
aiUpstream?: { baseUrl: string; fetch: typeof fetch };
|
|
284
|
+
/**
|
|
285
|
+
* The broker's engines run in the browser SharedWorker, which has no server
|
|
286
|
+
* environment: the gemini engine sends through the dev server's gemini
|
|
287
|
+
* proxy, which attaches the key (#1584). Set by the worker entry only; a
|
|
288
|
+
* Node host and a Node repro replay leave it unset.
|
|
289
|
+
*/
|
|
290
|
+
aiRunsInBrowser?: boolean;
|
|
281
291
|
/**
|
|
282
292
|
* Lazily-built agent tool dispatcher (the canonical sandbox tool set) bound to
|
|
283
293
|
* THIS worker's sandbox. The bridge peer forwards `tool` messages here so the
|
|
@@ -495,10 +495,21 @@ export function assertOperationArguments(message: Record<string, unknown>): void
|
|
|
495
495
|
}
|
|
496
496
|
|
|
497
497
|
|
|
498
|
+
/** An App Check claim is absent (a handle with no app) or a record whose token is a string or null. */
|
|
499
|
+
function assertAiAppCheck(appCheck: unknown): void {
|
|
500
|
+
const hasNoClaim = appCheck === undefined;
|
|
501
|
+
if (hasNoClaim) return;
|
|
502
|
+
requireRecord(appCheck, 'appCheck');
|
|
503
|
+
const token = appCheck.token;
|
|
504
|
+
const hasTokenShape = token === null || typeof token === 'string';
|
|
505
|
+
requireShape(hasTokenShape, 'appCheck.token');
|
|
506
|
+
}
|
|
507
|
+
|
|
498
508
|
/** Unary and streaming AI requests share the same adapter-owned wire fields. */
|
|
499
509
|
export function assertAiArguments(message: Record<string, unknown>): void {
|
|
500
510
|
requireString(message.model, 'model');
|
|
501
511
|
requireRecord(message.request, 'request');
|
|
512
|
+
assertAiAppCheck(message.appCheck);
|
|
502
513
|
const engine = message.engine;
|
|
503
514
|
const hasNoEngine = engine === undefined;
|
|
504
515
|
if (hasNoEngine) return;
|
|
@@ -59,6 +59,17 @@ export type AiEngineConfigWire =
|
|
|
59
59
|
}
|
|
60
60
|
| { kind: 'gemini'; baseUrl?: string; apiKey?: string };
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The App Check claim an AI request from an app-backed handle carries, as
|
|
64
|
+
* production reads `X-Firebase-AppCheck`: a token, or `null` when the handle
|
|
65
|
+
* has none. The worker broker refuses a request whose claim has no valid
|
|
66
|
+
* sandbox token. Requests from handles with no app send no claim and are not
|
|
67
|
+
* gated.
|
|
68
|
+
*/
|
|
69
|
+
export interface AiAppCheckWire {
|
|
70
|
+
token: string | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
62
73
|
/**
|
|
63
74
|
* Wire form of the Gemini error envelope an `AiBrokerError` carries.
|
|
64
75
|
*/
|
|
@@ -289,8 +300,8 @@ export type OpMessage = (
|
|
|
289
300
|
*/
|
|
290
301
|
| ({ t: 'op'; id: string; method: 'storage.createUploadSession'; path: string; contentType?: string; metadata?: Record<string, unknown>; downloadTokens?: string; origin?: string } & StorageBucketWire)
|
|
291
302
|
// AI ops
|
|
292
|
-
| { t: 'op'; id: string; method: 'ai.generateContent'; model: string; request: Record<string, unknown>; engine?: AiEngineConfigWire }
|
|
293
|
-
| { t: 'op'; id: string; method: 'ai.countTokens'; model: string; request: Record<string, unknown>; engine?: AiEngineConfigWire }
|
|
303
|
+
| { t: 'op'; id: string; method: 'ai.generateContent'; model: string; request: Record<string, unknown>; engine?: AiEngineConfigWire; appCheck?: AiAppCheckWire }
|
|
304
|
+
| { t: 'op'; id: string; method: 'ai.countTokens'; model: string; request: Record<string, unknown>; engine?: AiEngineConfigWire; appCheck?: AiAppCheckWire }
|
|
294
305
|
| { t: 'op'; id: string; method: 'getRuntimeEpoch' }
|
|
295
306
|
| { t: 'op'; id: string; method: 'retireRuntime'; targetEpoch: string }
|
|
296
307
|
| { t: 'op'; id: string; method: 'getVersion' }
|
|
@@ -377,6 +388,7 @@ export interface AiStreamSubMessage {
|
|
|
377
388
|
model: string;
|
|
378
389
|
request: Record<string, unknown>;
|
|
379
390
|
engine?: AiEngineConfigWire;
|
|
391
|
+
appCheck?: AiAppCheckWire;
|
|
380
392
|
}
|
|
381
393
|
|
|
382
394
|
export interface MessagingSubMessage {
|