@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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production AI mode's App Check verifier for a registered Node process
|
|
3
|
+
* (#1552): firebase-admin's own `verifyToken`, for the tokens the page gets
|
|
4
|
+
* from Firebase.
|
|
5
|
+
*
|
|
6
|
+
* The register installs it behind `pyric.admin.appCheck.firebaseVerifier` in
|
|
7
|
+
* production AI mode only, and loads this module on the first token whose
|
|
8
|
+
* `kid` is not the sandbox's, so a server that never sees one never loads
|
|
9
|
+
* firebase-admin. `firebase-admin/app` and `firebase-admin/app-check` reach
|
|
10
|
+
* the real package from here (`isBypassedBridgeImport`).
|
|
11
|
+
*
|
|
12
|
+
* Each project gets one firebase-admin app whose credential refuses to mint an
|
|
13
|
+
* access token, so the app cannot fall back to Application Default
|
|
14
|
+
* Credentials, the metadata server, or Google's OAuth endpoints. Verifying a
|
|
15
|
+
* token needs none of them: firebase-admin fetches Firebase's public keys from
|
|
16
|
+
* `https://firebaseappcheck.googleapis.com/v1/jwks` without credentials and
|
|
17
|
+
* caches them for six hours. `consume` is never passed here; the mirror
|
|
18
|
+
* records it in the process.
|
|
19
|
+
*
|
|
20
|
+
* The export is typed loosely on purpose: the published declarations of
|
|
21
|
+
* @pyric/cli carry no Firebase SDK edge. pyric-admin types the result.
|
|
22
|
+
*/
|
|
23
|
+
import { initializeApp, type App, type Credential } from 'firebase-admin/app';
|
|
24
|
+
import { getAppCheck } from 'firebase-admin/app-check';
|
|
25
|
+
|
|
26
|
+
const NO_CREDENTIALS = "Pyric's App Check verifier holds no credentials.";
|
|
27
|
+
|
|
28
|
+
/** A credential that never mints an access token. */
|
|
29
|
+
const noCredentials: Credential = {
|
|
30
|
+
getAccessToken: () => Promise.reject(new Error(NO_CREDENTIALS)),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const appsByProject = new Map<string, App>();
|
|
34
|
+
|
|
35
|
+
function verifierApp(projectId: string): App {
|
|
36
|
+
const existing = appsByProject.get(projectId);
|
|
37
|
+
if (existing !== undefined) return existing;
|
|
38
|
+
const app = initializeApp({ projectId, credential: noCredentials }, `[pyric] app-check verifier ${projectId}`);
|
|
39
|
+
appsByProject.set(projectId, app);
|
|
40
|
+
return app;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** firebase-admin's `verifyToken(token)` for an app scoped to `projectId`, with no options. */
|
|
44
|
+
export function verifyFirebaseAppCheckToken(token: string, projectId: string): Promise<unknown> {
|
|
45
|
+
return getAppCheck(verifierApp(projectId)).verifyToken(token);
|
|
46
|
+
}
|
|
@@ -20,6 +20,10 @@ import { shadowAppOf } from './app-bridge-shadow.js';
|
|
|
20
20
|
|
|
21
21
|
type RealInitialize = typeof realAppCheck.initializeAppCheck;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* `shadowAppOf` throws when the shadow app could not be created, so that
|
|
25
|
+
* cause surfaces here before the SDK would fail on a missing container.
|
|
26
|
+
*/
|
|
23
27
|
export function initializeAppCheck(app: { name: string } | undefined, options: unknown): unknown {
|
|
24
28
|
const shadow = app === undefined ? undefined : shadowAppOf(app);
|
|
25
29
|
return realAppCheck.initializeAppCheck(shadow as Parameters<RealInitialize>[0], options as Parameters<RealInitialize>[1]);
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Production AI pass-through for canonical `firebase/app` in a registered Node
|
|
3
3
|
* process: each Pyric app gets a shadow real Firebase app whose container the
|
|
4
4
|
* real `firebase/ai` reads. Shared by the in-process adapter
|
|
5
|
-
* (`./app-bridge.ts`)
|
|
5
|
+
* (`./app-bridge.ts`), the Node host client (`./client/app-bridge.ts`), and
|
|
6
|
+
* the served `firebase/app` entry (`serve/entries/app-ai-passthrough.ts`).
|
|
6
7
|
*/
|
|
7
8
|
import * as realApp from 'firebase/app';
|
|
8
9
|
import type { FirebaseApp, FirebaseAppSettings, FirebaseOptions } from 'pyric/app';
|
|
@@ -14,6 +15,27 @@ export interface PyricAppRegistry {
|
|
|
14
15
|
deleteApp(app: FirebaseApp): Promise<void>;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Why the shadow app for a name could not be created, kept until a later
|
|
20
|
+
* attempt succeeds or the app is deleted. It sits behind `Symbol.for` so every
|
|
21
|
+
* bundle or module record of this file on a page or in a process shares it.
|
|
22
|
+
*/
|
|
23
|
+
const SHADOW_FAILURES = Symbol.for('pyric.ai.shadowAppFailures');
|
|
24
|
+
type GlobalShadowFailures = { [SHADOW_FAILURES]?: Map<string, unknown> };
|
|
25
|
+
const shadowFailures = ((globalThis as GlobalShadowFailures)[SHADOW_FAILURES] ??= new Map<string, unknown>());
|
|
26
|
+
|
|
27
|
+
function causeText(cause: unknown): string {
|
|
28
|
+
const text = cause instanceof Error ? cause.message : String(cause);
|
|
29
|
+
return text.replace(/\.$/, '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shadowFailedError(name: string, cause: unknown): Error {
|
|
33
|
+
return new Error(
|
|
34
|
+
`Pyric could not create the Firebase app that firebase/app-check and firebase/ai attach to in production AI mode, for app "${name}": ${causeText(cause)}.`,
|
|
35
|
+
{ cause },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
function decorateApp(
|
|
18
40
|
app: FirebaseApp,
|
|
19
41
|
options?: FirebaseOptions,
|
|
@@ -34,24 +56,31 @@ function decorateApp(
|
|
|
34
56
|
shadow = realApp.initializeApp(initOptions, objectSettings) as { container?: unknown };
|
|
35
57
|
}
|
|
36
58
|
}
|
|
59
|
+
shadowFailures.delete(app.name);
|
|
37
60
|
const hasShadowContainer = shadow.container !== undefined && shadow.container !== null;
|
|
38
61
|
if (hasShadowContainer) {
|
|
39
62
|
Object.defineProperty(app, 'container', { value: shadow.container, configurable: true });
|
|
40
63
|
}
|
|
41
|
-
} catch {
|
|
42
|
-
//
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// Pyric's initializeApp still succeeds; shadowAppOf reports the cause
|
|
66
|
+
// when firebase/app-check or firebase/ai needs the shadow.
|
|
67
|
+
shadowFailures.set(app.name, error);
|
|
43
68
|
}
|
|
44
69
|
return app;
|
|
45
70
|
}
|
|
46
71
|
|
|
47
72
|
/**
|
|
48
|
-
* The shadow Firebase app for a Pyric app, by name.
|
|
49
|
-
*
|
|
73
|
+
* The shadow Firebase app for a Pyric app, by name. When creating it failed,
|
|
74
|
+
* throws an error that names the app and keeps the failure as `cause`. A
|
|
75
|
+
* Firebase app passed in directly, or a Pyric app with no attempt yet, is
|
|
76
|
+
* returned unchanged.
|
|
50
77
|
*/
|
|
51
78
|
export function shadowAppOf(app: { name: string }): object {
|
|
52
79
|
try {
|
|
53
80
|
return realApp.getApp(app.name);
|
|
54
81
|
} catch {
|
|
82
|
+
const hasFailure = shadowFailures.has(app.name);
|
|
83
|
+
if (hasFailure) throw shadowFailedError(app.name, shadowFailures.get(app.name));
|
|
55
84
|
return app;
|
|
56
85
|
}
|
|
57
86
|
}
|
|
@@ -69,6 +98,7 @@ export function shadowedAppRegistry(pyricApp: PyricAppRegistry): PyricAppRegistr
|
|
|
69
98
|
return pyricApp.getApps().map((app) => decorateApp(app));
|
|
70
99
|
},
|
|
71
100
|
async deleteApp(app) {
|
|
101
|
+
shadowFailures.delete(app.name);
|
|
72
102
|
try {
|
|
73
103
|
const shadow = realApp.getApp(app.name);
|
|
74
104
|
const hasShadow = shadow !== undefined && shadow !== null;
|
package/src/register/index.ts
CHANGED
|
@@ -38,12 +38,16 @@ import { resolveEsmOnlySubpath } from './esm-exports.js';
|
|
|
38
38
|
import { NET_GUARD_GLOBAL, installNetGuard, parseGuardMode, type GuardMode } from './net-guard.js';
|
|
39
39
|
import { emitBeacon } from './beacon.js';
|
|
40
40
|
import { remoteSandbox } from '../remote/index.js';
|
|
41
|
+
import { resolveAiMode } from '../serve/firebase-module-swap.js';
|
|
41
42
|
|
|
42
43
|
/** The shared seam with pyric-admin's ambient init: a synchronous factory
|
|
43
44
|
* returning the lazy branded handle. `Symbol.for` so both packages agree
|
|
44
45
|
* without importing each other's types. */
|
|
45
46
|
const SANDBOX_FACTORY = Symbol.for('pyric.remote.sandboxFactory');
|
|
46
47
|
|
|
48
|
+
/** The seam with pyric-admin's App Check mirror: firebase-admin's verifier for tokens from Firebase (#1552). */
|
|
49
|
+
const APP_CHECK_FIREBASE_VERIFIER = Symbol.for('pyric.admin.appCheck.firebaseVerifier');
|
|
50
|
+
|
|
47
51
|
interface SyncResolveContext {
|
|
48
52
|
parentURL?: string;
|
|
49
53
|
conditions?: string[];
|
|
@@ -170,6 +174,8 @@ function activate(): void {
|
|
|
170
174
|
(globalThis as Record<symbol, unknown>)[SANDBOX_FACTORY] = (opts?: { url?: string }) =>
|
|
171
175
|
remoteSandbox({ configuredUrl: opts?.url, instanceId: process.env.PYRIC_SANDBOX_INSTANCE });
|
|
172
176
|
|
|
177
|
+
installAppCheckVerifier();
|
|
178
|
+
|
|
173
179
|
process.stderr.write(
|
|
174
180
|
`@pyric/cli/register: active. firebase-admin/firebase imports now resolve to the ` +
|
|
175
181
|
`pyric sandbox (PYRIC_SANDBOX=${process.env.PYRIC_SANDBOX}).\n`,
|
|
@@ -187,6 +193,23 @@ function activate(): void {
|
|
|
187
193
|
});
|
|
188
194
|
}
|
|
189
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Production AI mode only: the page's App Check tokens come from Firebase, so
|
|
198
|
+
* pyric-admin's mirror hands them to firebase-admin's own verifier (#1552).
|
|
199
|
+
* The bridge, and firebase-admin with it, loads on the first such token, so a
|
|
200
|
+
* server that never verifies one never loads firebase-admin. In sandbox AI
|
|
201
|
+
* mode nothing is installed, and the mirror refuses such a token with a
|
|
202
|
+
* message that says why.
|
|
203
|
+
*/
|
|
204
|
+
function installAppCheckVerifier(): void {
|
|
205
|
+
const isProductionAiMode = resolveAiMode(undefined, process.env) === 'production';
|
|
206
|
+
if (!isProductionAiMode) return;
|
|
207
|
+
(globalThis as Record<symbol, unknown>)[APP_CHECK_FIREBASE_VERIFIER] = async (token: string, projectId: string) => {
|
|
208
|
+
const bridge = await import('./app-bridge-admin-app-check.js');
|
|
209
|
+
return bridge.verifyFirebaseAppCheckToken(token, projectId);
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
190
213
|
/**
|
|
191
214
|
* The net-guard mode actually in force, for the beacon to report.
|
|
192
215
|
*
|
|
@@ -11,7 +11,8 @@ import { capTerminalText } from './ai-terminal-text.js';
|
|
|
11
11
|
* agent driving `pyric dev` in process therefore saw NOTHING.
|
|
12
12
|
*
|
|
13
13
|
* - `request_rejected`: a bad role, empty `contents`, a missing thought
|
|
14
|
-
* signature,
|
|
14
|
+
* signature, a request without a valid App Check token, or an engine
|
|
15
|
+
* that answered an error envelope.
|
|
15
16
|
* - `response_blocked`: the SILENT refusal. A safety / recitation /
|
|
16
17
|
* blocklist filter answers HTTP 200 with an empty candidate, so nothing
|
|
17
18
|
* throws anywhere and the app simply renders nothing.
|
|
@@ -73,6 +74,8 @@ interface AiRejectionRelayPayload {
|
|
|
73
74
|
code?: number;
|
|
74
75
|
/** The rejection reason: production's own message for shape errors. */
|
|
75
76
|
message: string;
|
|
77
|
+
/** Pyric's guidance beside production's message, such as the App Check gate's. */
|
|
78
|
+
hint?: string;
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
/** Shape POSTed for a `response_blocked` event. Carries the wire's own
|
|
@@ -148,6 +151,8 @@ function toRejectionPayload(event: ServiceMutation): AiRejectionRelayPayload | n
|
|
|
148
151
|
payload.engine = detailText(detail, 'engine');
|
|
149
152
|
payload.status = detailText(detail, 'status');
|
|
150
153
|
if (typeof detail.code === 'number') payload.code = detail.code;
|
|
154
|
+
const hint = detailText(detail, 'hint');
|
|
155
|
+
if (hint !== undefined) payload.hint = hint;
|
|
151
156
|
return payload;
|
|
152
157
|
}
|
|
153
158
|
|
package/src/serve/ai-proxy.ts
CHANGED
|
@@ -16,6 +16,8 @@ import type { AiEngineConfigWire } from './worker/protocol.js';
|
|
|
16
16
|
import { permitGuardHost } from '../register/net-guard.js';
|
|
17
17
|
import { FIREBASE_AI_LOGIC_HOST } from '../google-endpoints.js';
|
|
18
18
|
import type { AiMode } from './firebase-module-swap.js';
|
|
19
|
+
import { HOSTED_METHOD_BODY_LIMIT } from './hosted/method-protocol.js';
|
|
20
|
+
import { BODY_TOO_LARGE_CODE, collectRawBody } from '../bridge/server/peer.js';
|
|
19
21
|
|
|
20
22
|
/** The dev server's diagnostics throttle, as this module needs it: the
|
|
21
23
|
* denials route owns the instance, and the proxy shares it so a failing
|
|
@@ -30,6 +32,10 @@ export const AI_PROXY_DEFAULT_UPSTREAM = 'http://localhost:11434/v1';
|
|
|
30
32
|
/** The route the dev server mounts for OpenAI-compatible traffic. */
|
|
31
33
|
export const AI_PROXY_ROUTE = '/__pyric/ai-proxy';
|
|
32
34
|
|
|
35
|
+
/** Largest request body the proxy forwards: the hosted method route's limit,
|
|
36
|
+
* which already carries AI requests with inline images. */
|
|
37
|
+
export const AI_PROXY_BODY_LIMIT_BYTES = HOSTED_METHOD_BODY_LIMIT;
|
|
38
|
+
|
|
33
39
|
/**
|
|
34
40
|
* Request headers that must NOT be forwarded upstream: origin-sensitive
|
|
35
41
|
* browser context (`origin`/`referer`/`cookie`, since the upstream is a
|
|
@@ -280,6 +286,25 @@ export async function fetchAiUpstream(
|
|
|
280
286
|
});
|
|
281
287
|
}
|
|
282
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Read an AI proxy request's body, at most {@link AI_PROXY_BODY_LIMIT_BYTES}.
|
|
291
|
+
* Past the limit it answers 413, and on a broken upload 400, and returns
|
|
292
|
+
* null: the caller sends nothing upstream. Both proxy routes read through it.
|
|
293
|
+
*/
|
|
294
|
+
export async function readAiProxyBody(
|
|
295
|
+
req: IncomingMessage,
|
|
296
|
+
res: ServerResponse,
|
|
297
|
+
): Promise<Uint8Array<ArrayBuffer> | null> {
|
|
298
|
+
try {
|
|
299
|
+
return await collectRawBody(req, AI_PROXY_BODY_LIMIT_BYTES);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
const tooLarge = (error as { code?: unknown }).code === BODY_TOO_LARGE_CODE;
|
|
302
|
+
const status = tooLarge ? 413 : 400;
|
|
303
|
+
res.writeHead(status, { 'content-type': 'text/plain' }).end(tooLarge ? 'Payload Too Large' : 'Bad Request');
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
283
308
|
/**
|
|
284
309
|
* Handle `POST /__pyric/ai-proxy/<suffix>`: a same-origin passthrough to the
|
|
285
310
|
* configured OpenAI-compatible upstream, so the browser openai engine
|
|
@@ -290,7 +315,11 @@ export async function fetchAiUpstream(
|
|
|
290
315
|
* - POST only (the OpenAI chat-completions surface is POST); 405 otherwise.
|
|
291
316
|
* - the path suffix + query ride through verbatim
|
|
292
317
|
* (`/__pyric/ai-proxy/chat/completions` becomes `<upstream>/chat/completions`).
|
|
293
|
-
* - the
|
|
318
|
+
* - the caller must pass the namespace's loopback guard (Host, and an
|
|
319
|
+
* Origin naming this server when one is sent), which the namespace runs
|
|
320
|
+
* before this handler: the upstream may carry the developer's key.
|
|
321
|
+
* - the request body is forwarded verbatim, up to
|
|
322
|
+
* {@link AI_PROXY_BODY_LIMIT_BYTES} (413 past it); headers minus the
|
|
294
323
|
* origin-sensitive/hop-by-hop set above (`authorization` DOES forward,
|
|
295
324
|
* since upstreams may require a key).
|
|
296
325
|
* - the response body is STREAMED through chunk-by-chunk. SSE passthrough
|
|
@@ -319,17 +348,9 @@ export async function handleAiProxy(
|
|
|
319
348
|
const suffix = url.pathname.slice(AI_PROXY_ROUTE.length);
|
|
320
349
|
const target = `${upstreamBase}${suffix}${url.search}`;
|
|
321
350
|
|
|
322
|
-
//
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
req.on('data', (c: Buffer) => chunks.push(c));
|
|
326
|
-
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
327
|
-
req.on('error', reject);
|
|
328
|
-
});
|
|
329
|
-
// Copy into a plain-ArrayBuffer view; fetch's BodyInit typing rejects
|
|
330
|
-
// Buffer's ArrayBufferLike backing.
|
|
331
|
-
const body = new Uint8Array(raw.byteLength);
|
|
332
|
-
body.set(raw);
|
|
351
|
+
// The request body is a bounded JSON payload, read once; the RESPONSE streams.
|
|
352
|
+
const body = await readAiProxyBody(req, res);
|
|
353
|
+
if (body === null) return;
|
|
333
354
|
|
|
334
355
|
const headers: Record<string, string> = {};
|
|
335
356
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
@@ -355,6 +376,16 @@ export async function handleAiProxy(
|
|
|
355
376
|
return;
|
|
356
377
|
}
|
|
357
378
|
|
|
379
|
+
await relayAiUpstream(upstream, res);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Send an upstream's answer to the caller: its status and content type, then
|
|
384
|
+
* the body chunk by chunk. SSE passthrough must never buffer. A dropped
|
|
385
|
+
* client cancels the upstream read so an abandoned stream doesn't keep the
|
|
386
|
+
* upstream generating.
|
|
387
|
+
*/
|
|
388
|
+
export async function relayAiUpstream(upstream: Response, res: ServerResponse): Promise<void> {
|
|
358
389
|
const responseHeaders: Record<string, string> = { 'cache-control': 'no-store' };
|
|
359
390
|
const contentType = upstream.headers.get('content-type');
|
|
360
391
|
if (contentType) responseHeaders['content-type'] = contentType;
|
|
@@ -365,8 +396,6 @@ export async function handleAiProxy(
|
|
|
365
396
|
}
|
|
366
397
|
(res as ServerResponse & { flushHeaders?: () => void }).flushHeaders?.();
|
|
367
398
|
|
|
368
|
-
// Chunk-by-chunk passthrough. A dropped client cancels the upstream read
|
|
369
|
-
// so an abandoned SSE stream doesn't keep the upstream generating.
|
|
370
399
|
const reader = upstream.body.getReader();
|
|
371
400
|
res.on('close', () => {
|
|
372
401
|
void reader.cancel().catch(() => {});
|
|
@@ -30,6 +30,8 @@ export interface AiRejectionPayload {
|
|
|
30
30
|
code?: unknown;
|
|
31
31
|
/** The rejection reason: production's own message for shape errors. */
|
|
32
32
|
message?: unknown;
|
|
33
|
+
/** Pyric's guidance beside production's message, such as the App Check gate's. */
|
|
34
|
+
hint?: unknown;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/** Loosely-typed mirror of the AI blocked-response payload. Unlike a
|
|
@@ -99,10 +101,10 @@ function formatModelLine(model: unknown, engine: unknown): string | null {
|
|
|
99
101
|
/**
|
|
100
102
|
* Format a relayed AI broker rejection into the compact terminal block, the
|
|
101
103
|
* same ` [pyric] ...` idiom the denial relay and the ai-proxy warning print.
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* unit tests.
|
|
104
|
+
* The reason production itself gives, then the model (with the resolved
|
|
105
|
+
* engine, since a rejection reads very differently for `scripted` than for a
|
|
106
|
+
* real upstream), then the wire status, then pyric's guidance when the broker
|
|
107
|
+
* gave one (the App Check gate does). Exported for unit tests.
|
|
106
108
|
*/
|
|
107
109
|
export function formatAiRejectionBlock(payload: AiRejectionPayload): string {
|
|
108
110
|
const message = readText(payload.message) ?? 'request rejected';
|
|
@@ -117,6 +119,8 @@ export function formatAiRejectionBlock(payload: AiRejectionPayload): string {
|
|
|
117
119
|
if (code !== null) suffix = ` (${code})`;
|
|
118
120
|
lines.push(` ${label}${suffix}`);
|
|
119
121
|
}
|
|
122
|
+
const hint = readText(payload.hint);
|
|
123
|
+
if (hint !== null) lines.push(` ${hint}`);
|
|
120
124
|
return lines.join('\n');
|
|
121
125
|
}
|
|
122
126
|
|
|
@@ -37,6 +37,17 @@ export function capTerminalText(text: string): string {
|
|
|
37
37
|
const TERMINAL_UNSAFE_CHARS =
|
|
38
38
|
/[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069\ufeff]/g;
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Replace control, bidi-override and line-separator characters with spaces,
|
|
42
|
+
* and change nothing else: no flattening, no whitespace collapsing, no cut.
|
|
43
|
+
* For one line of text a reader copies out of the terminal, such as a
|
|
44
|
+
* pasteable command or JSON fixture, where the exact spacing and the whole
|
|
45
|
+
* length matter. The caller splits lines first and bounds the total.
|
|
46
|
+
*/
|
|
47
|
+
export function stripTerminalControls(text: string): string {
|
|
48
|
+
return text.replace(TERMINAL_UNSAFE_CHARS, ' ');
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
/**
|
|
41
52
|
* Flatten an upstream-authored string into ONE terminal-safe line.
|
|
42
53
|
*
|
package/src/serve/bundler.ts
CHANGED
|
@@ -28,6 +28,8 @@ import { dirname, join, relative, sep } from 'node:path';
|
|
|
28
28
|
import { fileURLToPath } from 'node:url';
|
|
29
29
|
import * as esbuild from 'esbuild';
|
|
30
30
|
|
|
31
|
+
import { bridgeImportEntryKey, REAL_SDK_ENTRIES } from './firebase-module-swap.js';
|
|
32
|
+
|
|
31
33
|
export { SDK_MODULES } from './firebase-module-swap.js';
|
|
32
34
|
|
|
33
35
|
/**
|
|
@@ -160,6 +162,145 @@ function nodeShimPlugin(): esbuild.Plugin {
|
|
|
160
162
|
};
|
|
161
163
|
}
|
|
162
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Send the shadow-app wrappers' own `firebase/app` and `firebase/app-check`
|
|
167
|
+
* imports to the Firebase SDK bundle `pyric sandbox` builds in production AI
|
|
168
|
+
* mode (`/__pyric/sdk/firebase-sdk-*.js`). Left bare, the import map would
|
|
169
|
+
* send them back to the wrappers themselves. The rule is
|
|
170
|
+
* `isBypassedBridgeImport`, which the Vite and register planes read too.
|
|
171
|
+
*/
|
|
172
|
+
function bridgeImportPlugin(): esbuild.Plugin {
|
|
173
|
+
return {
|
|
174
|
+
name: 'pyric-serve-bridge-imports',
|
|
175
|
+
setup(build) {
|
|
176
|
+
build.onResolve({ filter: /^firebase\// }, (args) => {
|
|
177
|
+
const key = bridgeImportEntryKey(args.path, args.importer);
|
|
178
|
+
if (key === undefined) return undefined;
|
|
179
|
+
return { path: `/__pyric/sdk/${key}.js`, external: true };
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── the project's Firebase SDK (production AI mode) ─────────────────
|
|
186
|
+
|
|
187
|
+
/** The project's resolved `firebase` package. */
|
|
188
|
+
export interface ProjectFirebase {
|
|
189
|
+
/** The package directory, as resolved from the project. */
|
|
190
|
+
root: string;
|
|
191
|
+
version: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Find the `firebase` package the project's own code resolves, walking up
|
|
196
|
+
* `node_modules` directories from `projectDir` as Node does. Throws a message
|
|
197
|
+
* that tells the user to install it when there is none.
|
|
198
|
+
*/
|
|
199
|
+
export function resolveProjectFirebase(projectDir: string): ProjectFirebase {
|
|
200
|
+
let dir = projectDir;
|
|
201
|
+
while (true) {
|
|
202
|
+
const manifest = join(dir, 'node_modules', 'firebase', 'package.json');
|
|
203
|
+
if (existsSync(manifest)) {
|
|
204
|
+
const pkg = JSON.parse(readFileSync(manifest, 'utf8')) as { version?: string };
|
|
205
|
+
return { root: dirname(manifest), version: pkg.version ?? '0.0.0' };
|
|
206
|
+
}
|
|
207
|
+
const parent = dirname(dir);
|
|
208
|
+
if (parent === dir) break;
|
|
209
|
+
dir = parent;
|
|
210
|
+
}
|
|
211
|
+
throw new Error(
|
|
212
|
+
`pyric sandbox: production AI mode (${productionModeSetting(process.env)}) serves the Firebase SDK from this project, ` +
|
|
213
|
+
`but no firebase package resolves from ${projectDir}. Install it with \`npm install firebase\`, ` +
|
|
214
|
+
`or unset ${productionModeSetting(process.env).split('=')[0]} to answer firebase/ai from the Pyric sandbox.`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The environment setting that turned production AI mode on, for messages.
|
|
220
|
+
* `PYRIC_AI_MODE=production` wins; otherwise the passthrough switch did it.
|
|
221
|
+
*/
|
|
222
|
+
export function productionModeSetting(env: NodeJS.ProcessEnv): string {
|
|
223
|
+
const isModeSet = env.PYRIC_AI_MODE === 'production';
|
|
224
|
+
return isModeSet ? 'PYRIC_AI_MODE=production' : 'PYRIC_AI_PASSTHROUGH=1';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface ProjectFirebasePlan {
|
|
228
|
+
/** Pass to `bundleSdk`; absent when the real-SDK build is skipped. */
|
|
229
|
+
projectFirebase?: { projectDir: string };
|
|
230
|
+
/** One line to log when the build is skipped. */
|
|
231
|
+
warning?: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Decide whether production AI mode builds the Firebase SDK. Plain
|
|
236
|
+
* `pyric sandbox` serves the page itself, so a missing `firebase` throws.
|
|
237
|
+
* With a child command the child resolves `firebase` on its own, so a
|
|
238
|
+
* missing one only skips the build and warns.
|
|
239
|
+
*/
|
|
240
|
+
export function planProjectFirebase(projectDir: string, hasChildCommand: boolean, env: NodeJS.ProcessEnv = process.env): ProjectFirebasePlan {
|
|
241
|
+
try {
|
|
242
|
+
resolveProjectFirebase(projectDir);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (!hasChildCommand) throw error;
|
|
245
|
+
return {
|
|
246
|
+
warning:
|
|
247
|
+
` ⚠ production AI mode (${productionModeSetting(env)}): no firebase package resolves from ${projectDir}, ` +
|
|
248
|
+
`so the Firebase SDK is not built. Pages the sandbox serves itself (import map) need firebase installed where pyric starts; ` +
|
|
249
|
+
`your command resolves firebase on its own.`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return { projectFirebase: { projectDir } };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
const REAL_SDK_NAMESPACE = 'pyric-firebase-sdk';
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Build `firebase/app`, `firebase/ai` and `firebase/app-check` from the
|
|
260
|
+
* project's `firebase` install into `outDir`, in one pass with splitting so
|
|
261
|
+
* the three share one `@firebase/app` component registry, as Vite's
|
|
262
|
+
* dependency optimizer does.
|
|
263
|
+
*/
|
|
264
|
+
async function bundleProjectFirebase(outDir: string, projectDir: string, firebase: ProjectFirebase, minify: boolean): Promise<void> {
|
|
265
|
+
const entryPoints: Record<string, string> = {};
|
|
266
|
+
for (const [specifier, key] of REAL_SDK_ENTRIES) entryPoints[key] = `${REAL_SDK_NAMESPACE}:${specifier}`;
|
|
267
|
+
const result = await esbuild.build({
|
|
268
|
+
entryPoints,
|
|
269
|
+
outdir: outDir,
|
|
270
|
+
bundle: true,
|
|
271
|
+
format: 'esm',
|
|
272
|
+
platform: 'browser',
|
|
273
|
+
splitting: true,
|
|
274
|
+
sourcemap: 'linked',
|
|
275
|
+
minify,
|
|
276
|
+
logLevel: 'silent',
|
|
277
|
+
chunkNames: 'firebase-sdk-[hash]',
|
|
278
|
+
banner: {
|
|
279
|
+
js: `/* The Firebase JS SDK, firebase@${firebase.version} from this project, built by pyric sandbox for production AI mode. */`,
|
|
280
|
+
},
|
|
281
|
+
plugins: [{
|
|
282
|
+
name: 'pyric-serve-project-firebase',
|
|
283
|
+
setup(build) {
|
|
284
|
+
build.onResolve({ filter: new RegExp(`^${REAL_SDK_NAMESPACE}:`) }, (args) => ({
|
|
285
|
+
path: args.path.slice(REAL_SDK_NAMESPACE.length + 1),
|
|
286
|
+
namespace: REAL_SDK_NAMESPACE,
|
|
287
|
+
}));
|
|
288
|
+
build.onLoad({ filter: /.*/, namespace: REAL_SDK_NAMESPACE }, (args) => ({
|
|
289
|
+
contents: `export * from ${JSON.stringify(args.path)};`,
|
|
290
|
+
resolveDir: projectDir,
|
|
291
|
+
loader: 'js',
|
|
292
|
+
}));
|
|
293
|
+
},
|
|
294
|
+
}],
|
|
295
|
+
});
|
|
296
|
+
const hasBuildErrors = result.errors.length > 0;
|
|
297
|
+
if (hasBuildErrors) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`pyric sandbox: building firebase@${firebase.version} for production AI mode failed:\n${result.errors.map((e) => e.text).join('\n')}`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
163
304
|
// ─── cache ────────────────────────────────────────────────────────────
|
|
164
305
|
|
|
165
306
|
export interface BundleOptions {
|
|
@@ -171,6 +312,12 @@ export interface BundleOptions {
|
|
|
171
312
|
/** Override the cache root (tests). Default `~/.pyric/serve-cache`. */
|
|
172
313
|
cacheRoot?: string;
|
|
173
314
|
minify?: boolean;
|
|
315
|
+
/**
|
|
316
|
+
* Production AI mode: also build the Firebase SDK from this project's own
|
|
317
|
+
* `firebase` install (`/__pyric/sdk/firebase-sdk-*.js`). Absent in sandbox
|
|
318
|
+
* AI mode, which neither builds nor serves it.
|
|
319
|
+
*/
|
|
320
|
+
projectFirebase?: { projectDir: string };
|
|
174
321
|
}
|
|
175
322
|
|
|
176
323
|
export interface BundleResult {
|
|
@@ -184,7 +331,7 @@ export interface BundleResult {
|
|
|
184
331
|
}
|
|
185
332
|
|
|
186
333
|
/** Bump when bundler logic changes in a way that must invalidate caches. */
|
|
187
|
-
const BUNDLER_REV =
|
|
334
|
+
const BUNDLER_REV = 10; // 10: wrappers import the project's Firebase SDK by URL; production keys cover it
|
|
188
335
|
|
|
189
336
|
const RESOLUTION_FILES = [
|
|
190
337
|
'bun.lock',
|
|
@@ -262,12 +409,23 @@ export function cacheKey(
|
|
|
262
409
|
h.update(name);
|
|
263
410
|
h.update(readFileSync(file, 'utf8'));
|
|
264
411
|
}
|
|
412
|
+
// Production AI mode also serves the project's Firebase SDK, so upgrading
|
|
413
|
+
// `firebase`, or a project resolving another copy, rebuilds it.
|
|
414
|
+
const projectFirebase = opts.projectFirebase;
|
|
415
|
+
if (projectFirebase) {
|
|
416
|
+
const firebase = resolveProjectFirebase(projectFirebase.projectDir);
|
|
417
|
+
h.update(`firebase-sdk\0${firebase.root}\0${firebase.version}`);
|
|
418
|
+
}
|
|
265
419
|
return `${version}-${h.digest('hex').slice(0, 12)}`;
|
|
266
420
|
}
|
|
267
421
|
|
|
268
422
|
// ─── the bundle step ──────────────────────────────────────────────────
|
|
269
423
|
|
|
270
424
|
export async function bundleSdk(opts: BundleOptions): Promise<BundleResult> {
|
|
425
|
+
// Resolved first, so a project without `firebase` fails before any build.
|
|
426
|
+
const projectFirebase = opts.projectFirebase
|
|
427
|
+
? { ...opts.projectFirebase, firebase: resolveProjectFirebase(opts.projectFirebase.projectDir) }
|
|
428
|
+
: undefined;
|
|
271
429
|
const root = pyricPackageRoot();
|
|
272
430
|
const version = pyricVersion(root);
|
|
273
431
|
const key = cacheKey(opts, version);
|
|
@@ -317,7 +475,7 @@ export async function bundleSdk(opts: BundleOptions): Promise<BundleResult> {
|
|
|
317
475
|
js: '/* pyric sandbox shim serving firebase/*. This is not the real Firebase SDK. */',
|
|
318
476
|
},
|
|
319
477
|
external: ['firebase/*'],
|
|
320
|
-
plugins: [pyricResolvePlugin(), nodeShimPlugin()],
|
|
478
|
+
plugins: [pyricResolvePlugin(), nodeShimPlugin(), bridgeImportPlugin()],
|
|
321
479
|
});
|
|
322
480
|
const hasBuildErrors = result.errors.length > 0;
|
|
323
481
|
if (hasBuildErrors) {
|
|
@@ -325,6 +483,9 @@ export async function bundleSdk(opts: BundleOptions): Promise<BundleResult> {
|
|
|
325
483
|
`pyric sandbox: SDK bundle failed:\n${result.errors.map((e) => e.text).join('\n')}`,
|
|
326
484
|
);
|
|
327
485
|
}
|
|
486
|
+
if (projectFirebase) {
|
|
487
|
+
await bundleProjectFirebase(outDir, projectFirebase.projectDir, projectFirebase.firebase, opts.minify ?? true);
|
|
488
|
+
}
|
|
328
489
|
writeFileSync(join(outDir, '.complete'), new Date().toISOString());
|
|
329
490
|
const files = readdirSync(outDir)
|
|
330
491
|
.filter((f) => f.endsWith('.js'))
|