pyric 0.1.0-alpha.20 → 0.1.0-alpha.21
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/README.md +64 -89
- package/dist/ai/blocked.d.ts +52 -0
- package/dist/ai/blocked.d.ts.map +1 -0
- package/dist/ai/blocked.js +83 -0
- package/dist/ai/blocked.js.map +1 -0
- package/dist/ai/broker/broker.d.ts +49 -0
- package/dist/ai/broker/broker.d.ts.map +1 -1
- package/dist/ai/broker/broker.js +90 -0
- package/dist/ai/broker/broker.js.map +1 -1
- package/dist/ai/broker/gemini-engine.d.ts +14 -1
- package/dist/ai/broker/gemini-engine.d.ts.map +1 -1
- package/dist/ai/broker/gemini-engine.js +30 -9
- package/dist/ai/broker/gemini-engine.js.map +1 -1
- package/dist/ai/broker/index.d.ts +2 -2
- package/dist/ai/broker/index.d.ts.map +1 -1
- package/dist/ai/broker/index.js +2 -2
- package/dist/ai/broker/index.js.map +1 -1
- package/dist/ai/broker/openai-engine.d.ts +10 -1
- package/dist/ai/broker/openai-engine.d.ts.map +1 -1
- package/dist/ai/broker/openai-engine.js +26 -4
- package/dist/ai/broker/openai-engine.js.map +1 -1
- package/dist/ai/broker/synthesizer.d.ts +14 -0
- package/dist/ai/broker/synthesizer.d.ts.map +1 -1
- package/dist/ai/broker/synthesizer.js +31 -0
- package/dist/ai/broker/synthesizer.js.map +1 -1
- package/dist/ai/broker/types.d.ts +24 -0
- package/dist/ai/broker/types.d.ts.map +1 -1
- package/dist/ai/internal.d.ts +5 -0
- package/dist/ai/internal.d.ts.map +1 -1
- package/dist/ai/internal.js +5 -0
- package/dist/ai/internal.js.map +1 -1
- package/dist/ai/response-helpers.d.ts.map +1 -1
- package/dist/ai/response-helpers.js +6 -24
- package/dist/ai/response-helpers.js.map +1 -1
- package/dist/analytics/index.d.ts +40 -0
- package/dist/analytics/index.d.ts.map +1 -0
- package/dist/analytics/index.js +25 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/app-check/index.d.ts +31 -0
- package/dist/app-check/index.d.ts.map +1 -0
- package/dist/app-check/index.js +20 -0
- package/dist/app-check/index.js.map +1 -0
- package/dist/database/controls.d.ts +1 -1
- package/dist/database/controls.js +1 -1
- package/dist/database/sandbox/rules-eval.d.ts +1 -1
- package/dist/database/sandbox/rules-eval.d.ts.map +1 -1
- package/dist/database/sandbox/rules-eval.js +2 -2
- package/dist/database/sandbox/rules-eval.js.map +1 -1
- package/dist/database/sandbox/write-plane.js +2 -2
- package/dist/database/sandbox/write-plane.js.map +1 -1
- package/dist/database/sandbox-controls.d.ts +12 -5
- package/dist/database/sandbox-controls.d.ts.map +1 -1
- package/dist/database/sandbox-controls.js +20 -9
- package/dist/database/sandbox-controls.js.map +1 -1
- package/dist/deferred/entry.d.ts +75 -0
- package/dist/deferred/entry.d.ts.map +1 -0
- package/dist/deferred/entry.js +143 -0
- package/dist/deferred/entry.js.map +1 -0
- package/dist/firestore/lite.d.ts +72 -0
- package/dist/firestore/lite.d.ts.map +1 -0
- package/dist/firestore/lite.js +22 -0
- package/dist/firestore/lite.js.map +1 -0
- package/dist/firestore/persistence.d.ts +2 -2
- package/dist/firestore/persistence.js +1 -1
- package/dist/functions/index.d.ts +29 -0
- package/dist/functions/index.d.ts.map +1 -0
- package/dist/functions/index.js +20 -0
- package/dist/functions/index.js.map +1 -0
- package/dist/performance/index.d.ts +22 -0
- package/dist/performance/index.d.ts.map +1 -0
- package/dist/performance/index.js +19 -0
- package/dist/performance/index.js.map +1 -0
- package/dist/remote-config/index.d.ts +40 -0
- package/dist/remote-config/index.d.ts.map +1 -0
- package/dist/remote-config/index.js +25 -0
- package/dist/remote-config/index.js.map +1 -0
- package/dist/rules/grammar/FirestoreValidator.d.ts.map +1 -1
- package/dist/rules/grammar/FirestoreValidator.js +43 -27
- package/dist/rules/grammar/FirestoreValidator.js.map +1 -1
- package/dist/rules/grammar/document-access-count.d.ts +24 -0
- package/dist/rules/grammar/document-access-count.d.ts.map +1 -0
- package/dist/rules/grammar/document-access-count.js +100 -0
- package/dist/rules/grammar/document-access-count.js.map +1 -0
- package/dist/rules/linter/ast-utils.d.ts +2 -6
- package/dist/rules/linter/ast-utils.d.ts.map +1 -1
- package/dist/rules/linter/ast-utils.js +2 -63
- package/dist/rules/linter/ast-utils.js.map +1 -1
- package/dist/rules/linter/hallucinations.d.ts +8 -0
- package/dist/rules/linter/hallucinations.d.ts.map +1 -1
- package/dist/rules/linter/hallucinations.js +121 -31
- package/dist/rules/linter/hallucinations.js.map +1 -1
- package/dist/rules/linter/linter.d.ts +10 -1
- package/dist/rules/linter/linter.d.ts.map +1 -1
- package/dist/rules/linter/linter.js +23 -9
- package/dist/rules/linter/linter.js.map +1 -1
- package/dist/rules/rtdb/grammar/simulator.d.ts.map +1 -1
- package/dist/rules/rtdb/grammar/simulator.js +13 -13
- package/dist/rules/rtdb/grammar/simulator.js.map +1 -1
- package/dist/rules/rtdb/simulation/handler.d.ts.map +1 -1
- package/dist/rules/rtdb/simulation/handler.js +96 -35
- package/dist/rules/rtdb/simulation/handler.js.map +1 -1
- package/dist/rules/simulator/document-lookups.d.ts +4 -0
- package/dist/rules/simulator/document-lookups.d.ts.map +1 -1
- package/dist/rules/simulator/document-lookups.js +42 -3
- package/dist/rules/simulator/document-lookups.js.map +1 -1
- package/dist/rules/simulator/eval-error.d.ts +11 -0
- package/dist/rules/simulator/eval-error.d.ts.map +1 -1
- package/dist/rules/simulator/eval-error.js +14 -0
- package/dist/rules/simulator/eval-error.js.map +1 -1
- package/dist/rules/simulator/evaluation-builtins.d.ts.map +1 -1
- package/dist/rules/simulator/evaluation-builtins.js +33 -8
- package/dist/rules/simulator/evaluation-builtins.js.map +1 -1
- package/dist/rules/simulator/evaluation-context.d.ts +12 -0
- package/dist/rules/simulator/evaluation-context.d.ts.map +1 -1
- package/dist/rules/simulator/evaluator.d.ts +1 -1
- package/dist/rules/simulator/evaluator.d.ts.map +1 -1
- package/dist/rules/simulator/evaluator.js +16 -4
- package/dist/rules/simulator/evaluator.js.map +1 -1
- package/dist/rules/simulator/handler.d.ts.map +1 -1
- package/dist/rules/simulator/handler.js +52 -5
- package/dist/rules/simulator/handler.js.map +1 -1
- package/dist/rules/simulator/lookup-budget.d.ts +37 -0
- package/dist/rules/simulator/lookup-budget.d.ts.map +1 -0
- package/dist/rules/simulator/lookup-budget.js +96 -0
- package/dist/rules/simulator/lookup-budget.js.map +1 -0
- package/dist/rules/stdlib-modules.d.ts +7 -0
- package/dist/rules/stdlib-modules.d.ts.map +1 -1
- package/dist/rules/stdlib-modules.js +11 -11
- package/dist/rules/stdlib-modules.js.map +1 -1
- package/dist/sandbox/admin-firestore/get-firestore.d.ts +6 -0
- package/dist/sandbox/admin-firestore/get-firestore.d.ts.map +1 -1
- package/dist/sandbox/admin-firestore/get-firestore.js +22 -3
- package/dist/sandbox/admin-firestore/get-firestore.js.map +1 -1
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +1 -1
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/persistence/types.d.ts +1 -1
- package/dist/sandbox/remote.d.ts +2 -2
- package/dist/sandbox/sandbox-context.d.ts +8 -1
- package/dist/sandbox/sandbox-context.d.ts.map +1 -1
- package/dist/sandbox/sandbox-context.js +59 -8
- package/dist/sandbox/sandbox-context.js.map +1 -1
- package/dist/sandbox/types/auth-state.d.ts +1 -0
- package/dist/sandbox/types/auth-state.d.ts.map +1 -1
- package/dist/sandbox/types/events.d.ts +2 -1
- package/dist/sandbox/types/events.d.ts.map +1 -1
- package/dist/sandbox/types/operation.d.ts +1 -0
- package/dist/sandbox/types/operation.d.ts.map +1 -1
- package/dist/storage/enforce.d.ts +20 -1
- package/dist/storage/enforce.d.ts.map +1 -1
- package/dist/storage/enforce.js +47 -14
- package/dist/storage/enforce.js.map +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/list.js +1 -1
- package/dist/storage/list.js.map +1 -1
- package/dist/storage/sandbox/rules-evaluation-error.d.ts +25 -0
- package/dist/storage/sandbox/rules-evaluation-error.d.ts.map +1 -1
- package/dist/storage/sandbox/rules-evaluation-error.js +29 -0
- package/dist/storage/sandbox/rules-evaluation-error.js.map +1 -1
- package/dist/storage/sandbox/rules-evaluator.d.ts +10 -5
- package/dist/storage/sandbox/rules-evaluator.d.ts.map +1 -1
- package/dist/storage/sandbox/rules-evaluator.js +105 -33
- package/dist/storage/sandbox/rules-evaluator.js.map +1 -1
- package/dist/storage/sandbox/rules-methods.d.ts.map +1 -1
- package/dist/storage/sandbox/rules-methods.js +17 -7
- package/dist/storage/sandbox/rules-methods.js.map +1 -1
- package/dist/storage/service.d.ts +23 -2
- package/dist/storage/service.d.ts.map +1 -1
- package/dist/storage/service.js +36 -26
- package/dist/storage/service.js.map +1 -1
- package/package.json +41 -1
- package/src/ai/blocked.ts +97 -0
- package/src/ai/broker/broker.ts +88 -0
- package/src/ai/broker/gemini-engine.ts +36 -9
- package/src/ai/broker/index.ts +2 -1
- package/src/ai/broker/openai-engine.ts +31 -3
- package/src/ai/broker/synthesizer.ts +34 -0
- package/src/ai/broker/types.ts +25 -0
- package/src/ai/internal.ts +5 -0
- package/src/ai/response-helpers.ts +6 -25
- package/src/analytics/index.ts +53 -0
- package/src/app/dispatch.test.ts +3 -1
- package/src/app-check/index.ts +41 -0
- package/src/database/controls.ts +1 -1
- package/src/database/sandbox/rules-eval.ts +2 -2
- package/src/database/sandbox/write-plane.ts +2 -2
- package/src/database/sandbox-controls.ts +29 -9
- package/src/deferred/entry.ts +167 -0
- package/src/firestore/lite.ts +91 -0
- package/src/firestore/persistence.ts +2 -2
- package/src/functions/index.ts +38 -0
- package/src/performance/index.ts +31 -0
- package/src/remote-config/index.ts +53 -0
- package/src/rules/grammar/FirestoreValidator.ts +44 -28
- package/src/rules/grammar/document-access-count.ts +75 -0
- package/src/rules/linter/ast-utils.ts +2 -36
- package/src/rules/linter/hallucinations.ts +123 -25
- package/src/rules/linter/linter.ts +32 -10
- package/src/rules/rtdb/grammar/simulator.ts +12 -13
- package/src/rules/rtdb/simulation/handler.ts +149 -46
- package/src/rules/simulator/document-lookups.ts +46 -3
- package/src/rules/simulator/eval-error.ts +16 -0
- package/src/rules/simulator/evaluation-builtins.ts +35 -8
- package/src/rules/simulator/evaluation-context.ts +12 -0
- package/src/rules/simulator/evaluator.ts +14 -3
- package/src/rules/simulator/handler.ts +68 -4
- package/src/rules/simulator/lookup-budget.ts +101 -0
- package/src/rules/stdlib-modules.ts +19 -11
- package/src/sandbox/admin-firestore/get-firestore.ts +19 -3
- package/src/sandbox/index.ts +5 -1
- package/src/sandbox/persistence/types.ts +1 -1
- package/src/sandbox/remote.ts +2 -2
- package/src/sandbox/sandbox-context.ts +69 -9
- package/src/sandbox/types/auth-state.ts +1 -1
- package/src/sandbox/types/events.ts +2 -1
- package/src/sandbox/types/operation.ts +1 -1
- package/src/storage/enforce.ts +53 -14
- package/src/storage/index.ts +1 -1
- package/src/storage/list.ts +1 -1
- package/src/storage/sandbox/rules-evaluation-error.ts +32 -0
- package/src/storage/sandbox/rules-evaluator.ts +109 -32
- package/src/storage/sandbox/rules-methods.ts +21 -7
- package/src/storage/service.ts +92 -27
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
Synthesizer,
|
|
30
30
|
errorEnvelope,
|
|
31
31
|
estimateTokens,
|
|
32
|
+
redactUrl,
|
|
32
33
|
resolveModelVersion,
|
|
33
34
|
} from './synthesizer.js';
|
|
34
35
|
import { promptTextOf } from './scripted-engine.js';
|
|
@@ -37,6 +38,7 @@ import type {
|
|
|
37
38
|
CountTokensRequest,
|
|
38
39
|
CountTokensResponse,
|
|
39
40
|
GenerateContentRequest,
|
|
41
|
+
ModelResolution,
|
|
40
42
|
WireChunk,
|
|
41
43
|
WirePart,
|
|
42
44
|
WireResponse,
|
|
@@ -487,10 +489,27 @@ export class OpenAiEngine implements AnswerEngine {
|
|
|
487
489
|
);
|
|
488
490
|
}
|
|
489
491
|
|
|
492
|
+
/**
|
|
493
|
+
* Gemini model id to upstream model, WITH the reason: `modelMap[model]` ??
|
|
494
|
+
* `config.model` ?? passthrough. The reason is what makes the redirect
|
|
495
|
+
* legible in the dev terminal: "which of my three knobs sent this
|
|
496
|
+
* elsewhere?" is the whole question when the answer came from a model the
|
|
497
|
+
* developer never named. Bare (never `models/`-prefixed), per
|
|
498
|
+
* {@link AnswerEngine.resolveEffectiveModel}.
|
|
499
|
+
*/
|
|
500
|
+
resolveEffectiveModel(model: string): ModelResolution {
|
|
501
|
+
const bare = model.startsWith('models/') ? model.slice('models/'.length) : model;
|
|
502
|
+
const mapped = this.modelMap[bare] ?? this.modelMap[model];
|
|
503
|
+
if (mapped !== undefined) return { model: mapped, reason: 'engine modelMap' };
|
|
504
|
+
if (this.defaultModel !== undefined) {
|
|
505
|
+
return { model: this.defaultModel, reason: 'engine catch-all model' };
|
|
506
|
+
}
|
|
507
|
+
return { model: bare, reason: 'passthrough' };
|
|
508
|
+
}
|
|
509
|
+
|
|
490
510
|
/** Gemini model id → upstream model: modelMap[model] ?? config.model ?? passthrough. */
|
|
491
511
|
resolveUpstreamModel(model: string): string {
|
|
492
|
-
|
|
493
|
-
return this.modelMap[bare] ?? this.modelMap[model] ?? this.defaultModel ?? bare;
|
|
512
|
+
return this.resolveEffectiveModel(model).model;
|
|
494
513
|
}
|
|
495
514
|
|
|
496
515
|
async generateContent(req: GenerateContentRequest, model: string): Promise<WireResponse> {
|
|
@@ -627,8 +646,17 @@ export class OpenAiEngine implements AnswerEngine {
|
|
|
627
646
|
body: JSON.stringify(body),
|
|
628
647
|
});
|
|
629
648
|
} catch (err) {
|
|
649
|
+
// The openai engine has no key-in-URL auth mechanism today
|
|
650
|
+
// (`OpenAiEngineOptions` carries no `apiKey`), but a caller-supplied
|
|
651
|
+
// `baseUrl` could embed a credential in its query string, and some
|
|
652
|
+
// fetch implementations echo the request URL into their error text —
|
|
653
|
+
// redact defensively at this choke point (T1.7).
|
|
630
654
|
throw new AiBrokerError(
|
|
631
|
-
errorEnvelope(
|
|
655
|
+
errorEnvelope(
|
|
656
|
+
502,
|
|
657
|
+
`openai engine: upstream fetch failed: ${redactUrl(String(err))}`,
|
|
658
|
+
'UNAVAILABLE',
|
|
659
|
+
),
|
|
632
660
|
);
|
|
633
661
|
}
|
|
634
662
|
if (!res.ok) {
|
|
@@ -117,6 +117,40 @@ export function errorEnvelope(code: number, message: string, status: string): Wi
|
|
|
117
117
|
return { error: { code, message, status } };
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// ── URL redaction (T1.7) ─────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Query-param names that carry plaintext credentials on upstream REST
|
|
124
|
+
* requests (Google AI Studio's `?key=...` auth is the known case; the rest
|
|
125
|
+
* are defensive for sibling engines / future upstreams).
|
|
126
|
+
*
|
|
127
|
+
* Matched with a direct regex over the raw string rather than
|
|
128
|
+
* `new URL(...).searchParams` — the streaming action string embeds its own
|
|
129
|
+
* `?alt=sse` before the `?key=` is appended (a pre-existing, unrelated
|
|
130
|
+
* quirk this fix does not touch), which the strict `URLSearchParams` parser
|
|
131
|
+
* would fold into a single malformed pair and fail to redact. A regex on
|
|
132
|
+
* "`[?&]<param>=`" catches the value regardless of how the surrounding
|
|
133
|
+
* query string is (mal)formed.
|
|
134
|
+
*/
|
|
135
|
+
const SENSITIVE_URL_PARAM_PATTERN = /([?&](?:key|apiKey|api_key|access_token)=)[^&\s]*/gi;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Redacts credential-bearing query-string VALUES from a URL (or any string
|
|
139
|
+
* that may embed one) before it lands in an error message or log line —
|
|
140
|
+
* e.g. `?key=AIza...` becomes `?key=***`. Host and path are preserved so
|
|
141
|
+
* the message stays useful for diagnosing connectivity failures; only the
|
|
142
|
+
* secret value is masked.
|
|
143
|
+
*
|
|
144
|
+
* Used at every choke point where an upstream request URL is interpolated
|
|
145
|
+
* into a thrown error or logged text, so a leaked key never reaches
|
|
146
|
+
* terminal output, CI logs, or Studio traffic captures. Safe to apply
|
|
147
|
+
* defensively to values that are not themselves URLs (e.g. a raw fetch
|
|
148
|
+
* error message) — it is a no-op when no sensitive param is present.
|
|
149
|
+
*/
|
|
150
|
+
export function redactUrl(url: string): string {
|
|
151
|
+
return url.replace(SENSITIVE_URL_PARAM_PATTERN, '$1***');
|
|
152
|
+
}
|
|
153
|
+
|
|
120
154
|
/** `ai-error-unknown-model` (404 NOT_FOUND), captured text verbatim — including production's `v1main`. */
|
|
121
155
|
export function unknownModel(name: string): WireErrorEnvelope {
|
|
122
156
|
return errorEnvelope(
|
package/src/ai/broker/types.ts
CHANGED
|
@@ -136,6 +136,31 @@ export interface AnswerEngine {
|
|
|
136
136
|
generateContent(req: GenerateContentRequest, model: string): Promise<WireResponse>;
|
|
137
137
|
streamGenerateContent(req: GenerateContentRequest, model: string): AsyncIterable<WireChunk>;
|
|
138
138
|
countTokens(req: CountTokensRequest, model: string): Promise<CountTokensResponse>;
|
|
139
|
+
/**
|
|
140
|
+
* OPTIONAL self-report: which model this engine will ACTUALLY answer with
|
|
141
|
+
* for a requested Gemini model id, and why it differs. Engines that redirect
|
|
142
|
+
* silently (an openai `modelMap` entry or catch-all `model`, a gemini
|
|
143
|
+
* experimental alias) implement it so the broker can announce the swap on
|
|
144
|
+
* the event stream. A developer must never believe they tested model X when
|
|
145
|
+
* model Y answered.
|
|
146
|
+
*
|
|
147
|
+
* Engines that never substitute (scripted, a caller's custom engine) omit
|
|
148
|
+
* the method entirely, and the broker stays silent for them.
|
|
149
|
+
*/
|
|
150
|
+
resolveEffectiveModel?(model: string): ModelResolution;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Which model an engine answers with, and why it differs from the one the
|
|
155
|
+
* request named.
|
|
156
|
+
*
|
|
157
|
+
* `model` is BARE (no `models/` prefix) so the broker can compare it against
|
|
158
|
+
* the requested id without the prefix reading as a substitution. `reason` is
|
|
159
|
+
* a short phrase for the terminal line, e.g. `engine modelMap`.
|
|
160
|
+
*/
|
|
161
|
+
export interface ModelResolution {
|
|
162
|
+
model: string;
|
|
163
|
+
reason: string;
|
|
139
164
|
}
|
|
140
165
|
|
|
141
166
|
// ── Scripted authoring ──────────────────────────────────────────────────────
|
package/src/ai/internal.ts
CHANGED
|
@@ -6,6 +6,11 @@ import { TARGET_SYMBOL } from './target.js';
|
|
|
6
6
|
import type { AI, AIOptions, TransportTarget } from './types.js';
|
|
7
7
|
|
|
8
8
|
export { aiErrorFromEnvelope } from './errors.js';
|
|
9
|
+
/** Host-only seam for the dev server's terminal diagnostics: the credential
|
|
10
|
+
* masking every printed upstream URL goes through, and the gemini endpoint
|
|
11
|
+
* the startup banner names when no `baseUrl` overrides it. Published here so
|
|
12
|
+
* `@pyric/cli` reads one definition instead of copying both. */
|
|
13
|
+
export { GEMINI_DEFAULT_BASE_URL, redactUrl } from './broker/index.js';
|
|
9
14
|
|
|
10
15
|
const transportHandles = new WeakMap<FirebaseApp, Map<string, AI>>();
|
|
11
16
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import type { WireCandidate, WirePart, WireResponse } from './broker/index.js';
|
|
23
23
|
import { AIError, AIErrorCode } from './errors.js';
|
|
24
|
-
import {
|
|
24
|
+
import { isBlockingFinishReason } from './blocked.js';
|
|
25
25
|
|
|
26
26
|
/** The wire envelope plus the SDK's helper methods. */
|
|
27
27
|
export interface EnhancedResponse extends WireResponse {
|
|
@@ -31,28 +31,9 @@ export interface EnhancedResponse extends WireResponse {
|
|
|
31
31
|
functionCalls(): Array<NonNullable<WirePart['functionCall']>> | undefined;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
FinishReason.BLOCKLIST,
|
|
38
|
-
FinishReason.PROHIBITED_CONTENT,
|
|
39
|
-
FinishReason.SPII,
|
|
40
|
-
FinishReason.MALFORMED_FUNCTION_CALL,
|
|
41
|
-
FinishReason.IMAGE_SAFETY,
|
|
42
|
-
FinishReason.IMAGE_PROHIBITED_CONTENT,
|
|
43
|
-
FinishReason.IMAGE_OTHER,
|
|
44
|
-
FinishReason.NO_IMAGE,
|
|
45
|
-
FinishReason.IMAGE_RECITATION,
|
|
46
|
-
FinishReason.LANGUAGE,
|
|
47
|
-
FinishReason.UNEXPECTED_TOOL_CALL,
|
|
48
|
-
FinishReason.TOO_MANY_TOOL_CALLS,
|
|
49
|
-
FinishReason.MISSING_THOUGHT_SIGNATURE,
|
|
50
|
-
FinishReason.MALFORMED_RESPONSE,
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
function hadBadFinishReason(candidate: WireCandidate): boolean {
|
|
54
|
-
return !!candidate.finishReason && badFinishReasons.some((r) => r === candidate.finishReason);
|
|
55
|
-
}
|
|
34
|
+
// Upstream's `badFinishReasons` set lives in `blocked.ts`: the broker
|
|
35
|
+
// announces the SAME set on the event stream (`response_blocked`), and the
|
|
36
|
+
// two must never drift apart.
|
|
56
37
|
|
|
57
38
|
export function formatBlockErrorMessage(response: WireResponse): string {
|
|
58
39
|
let message = '';
|
|
@@ -63,7 +44,7 @@ export function formatBlockErrorMessage(response: WireResponse): string {
|
|
|
63
44
|
}
|
|
64
45
|
} else if (response.candidates?.[0]) {
|
|
65
46
|
const firstCandidate = response.candidates[0];
|
|
66
|
-
if (
|
|
47
|
+
if (isBlockingFinishReason(firstCandidate.finishReason)) {
|
|
67
48
|
message += `Candidate was blocked due to ${firstCandidate.finishReason}`;
|
|
68
49
|
if (firstCandidate.finishMessage) {
|
|
69
50
|
message += `: ${firstCandidate.finishMessage}`;
|
|
@@ -76,7 +57,7 @@ export function formatBlockErrorMessage(response: WireResponse): string {
|
|
|
76
57
|
/** At least one candidate exists and the first has no bad finish reason. */
|
|
77
58
|
function hasValidCandidates(response: WireResponse): boolean {
|
|
78
59
|
if (response.candidates && response.candidates.length > 0) {
|
|
79
|
-
if (
|
|
60
|
+
if (isBlockingFinishReason(response.candidates[0]!.finishReason)) {
|
|
80
61
|
throw new AIError(
|
|
81
62
|
AIErrorCode.RESPONSE_ERROR,
|
|
82
63
|
`Response error: ${formatBlockErrorMessage(response)}. Response body stored in error.response`,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pyric/analytics` — a DEFERRED mirror of `firebase/analytics`.
|
|
3
|
+
*
|
|
4
|
+
* Google Analytics for Firebase is a fire-and-forget telemetry pipe to
|
|
5
|
+
* Google servers. There is no observable local behavior for the sandbox to
|
|
6
|
+
* mirror, so it is deferred.
|
|
7
|
+
*
|
|
8
|
+
* Every symbol below resolves and links so an app that swaps `firebase` for
|
|
9
|
+
* `pyric` still loads; touching one throws `PyricDeferredApiError` naming this
|
|
10
|
+
* subpath. See `../deferred/entry.ts` for the full rationale.
|
|
11
|
+
*
|
|
12
|
+
* The value list is the exact public runtime surface of `firebase/analytics`
|
|
13
|
+
* (Firebase Web SDK 12.13.0). Keep it in sync when this entry graduates to a
|
|
14
|
+
* real mirror.
|
|
15
|
+
*/
|
|
16
|
+
import { deferredEntry, type DeferredApi } from '../deferred/entry.js';
|
|
17
|
+
|
|
18
|
+
export { PyricDeferredApiError } from '../deferred/entry.js';
|
|
19
|
+
|
|
20
|
+
export const {
|
|
21
|
+
getAnalytics, getGoogleAnalyticsClientId, initializeAnalytics, logEvent,
|
|
22
|
+
setAnalyticsCollectionEnabled, setConsent, setCurrentScreen, setDefaultEventParameters,
|
|
23
|
+
setUserId, setUserProperties, settings,
|
|
24
|
+
} = deferredEntry('analytics');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The one deferred symbol that answers instead of throwing: the real SDK
|
|
28
|
+
* resolves a boolean, and a deferred entry IS unsupported, so the standard
|
|
29
|
+
* `isSupported().then(ok => ok && get…(app))` guard must run — not crash.
|
|
30
|
+
*/
|
|
31
|
+
export const isSupported = async (): Promise<boolean> => false;
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
// Type declarations. Aliased to the deferred placeholder so a consumer's own
|
|
35
|
+
// annotations keep type-checking: every deferred call returns `never`, which is
|
|
36
|
+
// assignable to any of these. Names that Firebase exports as a CLASS appear both
|
|
37
|
+
// here and above — a class is a value and a type, and both meanings must survive
|
|
38
|
+
// the swap.
|
|
39
|
+
export type Analytics = DeferredApi;
|
|
40
|
+
export type AnalyticsCallOptions = DeferredApi;
|
|
41
|
+
export type AnalyticsSettings = DeferredApi;
|
|
42
|
+
export type ConsentSettings = DeferredApi;
|
|
43
|
+
export type ConsentStatusString = DeferredApi;
|
|
44
|
+
export type ControlParams = DeferredApi;
|
|
45
|
+
export type Currency = DeferredApi;
|
|
46
|
+
export type CustomEventName = DeferredApi;
|
|
47
|
+
export type CustomParams = DeferredApi;
|
|
48
|
+
export type EventNameString = DeferredApi;
|
|
49
|
+
export type EventParams = DeferredApi;
|
|
50
|
+
export type GtagConfigParams = DeferredApi;
|
|
51
|
+
export type Item = DeferredApi;
|
|
52
|
+
export type Promotion = DeferredApi;
|
|
53
|
+
export type SettingsOptions = DeferredApi;
|
package/src/app/dispatch.test.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
signInWithEmailAndPassword,
|
|
27
27
|
signOut,
|
|
28
28
|
} from '../auth/index.js';
|
|
29
|
-
import { get, getDatabase, ref, set } from '../database/index.js';
|
|
29
|
+
import { get, getDatabase, ref, sandbox as databaseSandbox, set } from '../database/index.js';
|
|
30
30
|
import { getStorage } from '../storage/index.js';
|
|
31
31
|
import { getAI } from '../ai/index.js';
|
|
32
32
|
import { getMessaging } from '../messaging/index.js';
|
|
@@ -121,6 +121,8 @@ describe('pyric/app — Firebase-shaped service containers', () => {
|
|
|
121
121
|
|
|
122
122
|
const dbA = getDatabase(a);
|
|
123
123
|
const dbB = getDatabase(b);
|
|
124
|
+
databaseSandbox.setDefaultPolicy(dbA, 'allow');
|
|
125
|
+
databaseSandbox.setDefaultPolicy(dbB, 'allow');
|
|
124
126
|
await set(ref(dbA, 'shared/value'), { from: authA.currentUser!.uid });
|
|
125
127
|
expect((await get(ref(dbB, 'shared/value'))).val()).toEqual({ from: authA.currentUser!.uid });
|
|
126
128
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pyric/app-check` — a DEFERRED mirror of `firebase/app-check`.
|
|
3
|
+
*
|
|
4
|
+
* App Check attests that a request comes from your app, via reCAPTCHA or a
|
|
5
|
+
* custom attestation provider talking to Google infrastructure. The sandbox
|
|
6
|
+
* has no attestation authority to model against, so the whole surface is
|
|
7
|
+
* deferred.
|
|
8
|
+
*
|
|
9
|
+
* Every symbol below resolves and links so an app that swaps `firebase` for
|
|
10
|
+
* `pyric` still loads; touching one throws `PyricDeferredApiError` naming this
|
|
11
|
+
* subpath. See `../deferred/entry.ts` for the full rationale.
|
|
12
|
+
*
|
|
13
|
+
* The value list is the exact public runtime surface of `firebase/app-check`
|
|
14
|
+
* (Firebase Web SDK 12.13.0). Keep it in sync when this entry graduates to a
|
|
15
|
+
* real mirror.
|
|
16
|
+
*/
|
|
17
|
+
import { deferredEntry, type DeferredApi } from '../deferred/entry.js';
|
|
18
|
+
|
|
19
|
+
export { PyricDeferredApiError } from '../deferred/entry.js';
|
|
20
|
+
|
|
21
|
+
export const {
|
|
22
|
+
CustomProvider, ReCaptchaEnterpriseProvider, ReCaptchaV3Provider, getLimitedUseToken,
|
|
23
|
+
getToken, initializeAppCheck, onTokenChanged, setTokenAutoRefreshEnabled,
|
|
24
|
+
} = deferredEntry('app-check');
|
|
25
|
+
|
|
26
|
+
// Type declarations. Aliased to the deferred placeholder so a consumer's own
|
|
27
|
+
// annotations keep type-checking: every deferred call returns `never`, which is
|
|
28
|
+
// assignable to any of these. Names that Firebase exports as a CLASS appear both
|
|
29
|
+
// here and above — a class is a value and a type, and both meanings must survive
|
|
30
|
+
// the swap.
|
|
31
|
+
export type AppCheck = DeferredApi;
|
|
32
|
+
export type AppCheckOptions = DeferredApi;
|
|
33
|
+
export type AppCheckToken = DeferredApi;
|
|
34
|
+
export type AppCheckTokenListener = DeferredApi;
|
|
35
|
+
export type AppCheckTokenResult = DeferredApi;
|
|
36
|
+
export type CustomProvider = DeferredApi;
|
|
37
|
+
export type CustomProviderOptions = DeferredApi;
|
|
38
|
+
export type PartialObserver = DeferredApi;
|
|
39
|
+
export type ReCaptchaEnterpriseProvider = DeferredApi;
|
|
40
|
+
export type ReCaptchaV3Provider = DeferredApi;
|
|
41
|
+
export type Unsubscribe = DeferredApi;
|
package/src/database/controls.ts
CHANGED
|
@@ -69,7 +69,7 @@ export function forceWebSockets(): void {
|
|
|
69
69
|
*
|
|
70
70
|
* Accepted no-op: the sandbox has no modular-SDK-style logger to wire a
|
|
71
71
|
* level/sink into (it uses host-level `console` logging directly, gated
|
|
72
|
-
* by `pyric
|
|
72
|
+
* by `pyric sandbox`'s own flags — matching `pyric/firestore`'s
|
|
73
73
|
* `setLogLevel`). Accepted so init code that calls it compiles + runs.
|
|
74
74
|
*/
|
|
75
75
|
export function enableLogging(
|
|
@@ -99,7 +99,7 @@ export type RtdbDefaultPolicy = 'allow' | 'deny';
|
|
|
99
99
|
|
|
100
100
|
export class RulesEvaluator {
|
|
101
101
|
private compiled: CompiledRtdbRules | null = null;
|
|
102
|
-
private defaultPolicy: RtdbDefaultPolicy = '
|
|
102
|
+
private defaultPolicy: RtdbDefaultPolicy = 'deny';
|
|
103
103
|
|
|
104
104
|
/** Set default access policy when no rules are loaded ('allow' or 'deny'). */
|
|
105
105
|
setDefaultPolicy(policy: RtdbDefaultPolicy): void {
|
|
@@ -122,7 +122,7 @@ export class RulesEvaluator {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* Evaluate one op. Default-
|
|
125
|
+
* Evaluate one op. Default-deny when no rules are loaded.
|
|
126
126
|
*
|
|
127
127
|
* Errors from the underlying simulator (grammar mismatches, etc.)
|
|
128
128
|
* surface as `'no-rule'` — they're treated as deny by user-mode
|
|
@@ -233,7 +233,7 @@ export class WritePlane {
|
|
|
233
233
|
});
|
|
234
234
|
for (const update of updates) {
|
|
235
235
|
if (this.state.rules.evaluate('write', update.path, {
|
|
236
|
-
auth, mockData, newData: update.value,
|
|
236
|
+
auth, mockData, newData: update.value, updates,
|
|
237
237
|
}).check !== 'allow') throw permissionDenied();
|
|
238
238
|
}
|
|
239
239
|
}
|
|
@@ -252,7 +252,7 @@ export class WritePlane {
|
|
|
252
252
|
const at = Date.now();
|
|
253
253
|
const before = this.state.tree.read(update.path);
|
|
254
254
|
const evaluation = this.state.rules.evaluate('write', update.path, {
|
|
255
|
-
auth, mockData, newData: update.value,
|
|
255
|
+
auth, mockData, newData: update.value, updates,
|
|
256
256
|
});
|
|
257
257
|
const fields = {
|
|
258
258
|
at, durationMs: Date.now() - at, origin: 'batch' as const,
|
|
@@ -2,34 +2,54 @@
|
|
|
2
2
|
import type { LocalSandbox } from 'pyric/sandbox';
|
|
3
3
|
|
|
4
4
|
import { getOrCreateBackend } from './sandbox/backend-for.js';
|
|
5
|
+
import type { RtdbBackend } from './sandbox/backend.js';
|
|
5
6
|
import type { JsonValue } from './sandbox/data-tree.js';
|
|
7
|
+
import { TARGET_SYMBOL, targetOf } from './routing.js';
|
|
6
8
|
|
|
7
9
|
export type RtdbRulesJson = { rules: Record<string, unknown> };
|
|
8
10
|
|
|
9
|
-
/**
|
|
11
|
+
/** Target accepting either a LocalSandbox root or a branded Database handle. */
|
|
12
|
+
export type RtdbTarget = LocalSandbox | { [TARGET_SYMBOL]: unknown };
|
|
13
|
+
|
|
14
|
+
function backendOf(target: RtdbTarget): RtdbBackend {
|
|
15
|
+
if (target && typeof target === 'object' && TARGET_SYMBOL in target) {
|
|
16
|
+
return targetOf(target).backend;
|
|
17
|
+
}
|
|
18
|
+
return getOrCreateBackend(target as LocalSandbox);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Set default access policy when no rules are loaded ('allow' or 'deny'). Internal test/dev harness control. */
|
|
22
|
+
export function setDefaultPolicy(
|
|
23
|
+
target: RtdbTarget,
|
|
24
|
+
policy: 'allow' | 'deny',
|
|
25
|
+
): void {
|
|
26
|
+
backendOf(target).setDefaultPolicy(policy);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Replace the active RTDB rules. Pass `null` to restore default deny. */
|
|
10
30
|
export function setRules(
|
|
11
|
-
|
|
31
|
+
target: RtdbTarget,
|
|
12
32
|
rules: RtdbRulesJson | null,
|
|
13
33
|
): void {
|
|
14
|
-
|
|
34
|
+
backendOf(target).setRules(rules);
|
|
15
35
|
}
|
|
16
36
|
|
|
17
37
|
/** Read the currently active rules as detached JSON. */
|
|
18
|
-
export function getActiveRules(
|
|
19
|
-
return
|
|
38
|
+
export function getActiveRules(target: RtdbTarget): RtdbRulesJson | null {
|
|
39
|
+
return backendOf(target).getActiveRules();
|
|
20
40
|
}
|
|
21
41
|
|
|
22
42
|
/** Replace RTDB data in bulk without applying security rules. */
|
|
23
43
|
export function setData(
|
|
24
|
-
|
|
44
|
+
target: RtdbTarget,
|
|
25
45
|
data: Record<string, unknown>,
|
|
26
46
|
): void {
|
|
27
|
-
|
|
47
|
+
backendOf(target).setData(data as Record<string, JsonValue>);
|
|
28
48
|
}
|
|
29
49
|
|
|
30
50
|
/** Snapshot the complete RTDB tree without applying security rules. */
|
|
31
|
-
export function snapshotState(
|
|
32
|
-
return
|
|
51
|
+
export function snapshotState(target: RtdbTarget): JsonValue {
|
|
52
|
+
return backendOf(target).snapshotState();
|
|
33
53
|
}
|
|
34
54
|
|
|
35
55
|
/**
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared factory behind pyric's *deferred* Firebase Web SDK subpaths —
|
|
3
|
+
* `pyric/functions`, `pyric/analytics`, `pyric/app-check`,
|
|
4
|
+
* `pyric/firestore/lite`, `pyric/performance` and `pyric/remote-config`.
|
|
5
|
+
*
|
|
6
|
+
* WHY THESE EXIST AT ALL. pyric is a symbol-for-symbol drop-in for the
|
|
7
|
+
* Firebase Web SDK: an app swaps the `firebase` specifier for `pyric` and its
|
|
8
|
+
* module graph must keep loading. A subpath that is simply absent from the
|
|
9
|
+
* package `exports` map fails at RESOLVE time — Node raises
|
|
10
|
+
* `ERR_PACKAGE_PATH_NOT_EXPORTED`, bundlers raise their own variants — long
|
|
11
|
+
* before any pyric code runs. That error names neither pyric nor the reason,
|
|
12
|
+
* so a developer whose app merely *mentions* `firebase/functions` on a code
|
|
13
|
+
* path it never takes sees an unattributable build failure.
|
|
14
|
+
*
|
|
15
|
+
* THE CONTRACT. Resolution and linking succeed; *use* fails loudly.
|
|
16
|
+
* - The subpath resolves, so the module graph loads.
|
|
17
|
+
* - Every symbol the real Firebase entry exports is present as a value, so
|
|
18
|
+
* named-import linking (which is static in ESM — a missing name is a link
|
|
19
|
+
* error, not a runtime one) succeeds and bundlers can tree-shake.
|
|
20
|
+
* - Calling, constructing, or reading a member off one of those values
|
|
21
|
+
* throws {@link PyricDeferredApiError} with a message that names the
|
|
22
|
+
* subpath and points at the conformance matrix.
|
|
23
|
+
*
|
|
24
|
+
* An app that imports but never calls therefore runs unchanged. An app that
|
|
25
|
+
* genuinely depends on a deferred service gets one clear, attributed error at
|
|
26
|
+
* the exact call site instead of a cryptic resolver failure.
|
|
27
|
+
*
|
|
28
|
+
* NOT a mirror. These entries deliberately implement nothing. They are marked
|
|
29
|
+
* deferred in the conformance surface story and must never be counted as
|
|
30
|
+
* implemented surface.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Compose the single user-facing message every deferred entry raises.
|
|
35
|
+
*
|
|
36
|
+
* @param subpath - The Firebase subpath *without* the `firebase/` prefix, e.g.
|
|
37
|
+
* `functions` or `firestore/lite`.
|
|
38
|
+
*/
|
|
39
|
+
function deferredApiMessage(subpath: string): string {
|
|
40
|
+
return (
|
|
41
|
+
`pyric: 'firebase/${subpath}' is not yet mirrored by the local sandbox. ` +
|
|
42
|
+
'This API is deferred — see the conformance matrix. Imports resolve so ' +
|
|
43
|
+
'module graphs load; calls fail with this message.'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The error thrown by every deferred Firebase entry. Carries the subpath and
|
|
49
|
+
* the symbol that was touched so tooling can group these without parsing the
|
|
50
|
+
* message.
|
|
51
|
+
*/
|
|
52
|
+
export class PyricDeferredApiError extends Error {
|
|
53
|
+
override readonly name = 'PyricDeferredApiError';
|
|
54
|
+
|
|
55
|
+
/** The Firebase subpath, without the `firebase/` prefix (e.g. `functions`). */
|
|
56
|
+
readonly subpath: string;
|
|
57
|
+
|
|
58
|
+
/** The exported symbol whose use triggered this error (e.g. `getFunctions`). */
|
|
59
|
+
readonly symbol: string;
|
|
60
|
+
|
|
61
|
+
constructor(subpath: string, symbol: string) {
|
|
62
|
+
super(deferredApiMessage(subpath));
|
|
63
|
+
this.subpath = subpath;
|
|
64
|
+
this.symbol = symbol;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The static type of a deferred export.
|
|
70
|
+
*
|
|
71
|
+
* It is callable, constructible, and indexable so a deferred symbol can stand
|
|
72
|
+
* in for any of the three shapes a Firebase entry exports — a factory function
|
|
73
|
+
* (`getFunctions`), a class (`ReCaptchaV3Provider`), or an enum-like constant
|
|
74
|
+
* object (`HarmCategory`). Every result is `never`, which is assignable to
|
|
75
|
+
* anything, so a consumer's own annotations keep type-checking.
|
|
76
|
+
*/
|
|
77
|
+
export interface DeferredApi {
|
|
78
|
+
(...args: readonly unknown[]): never;
|
|
79
|
+
new (...args: readonly unknown[]): never;
|
|
80
|
+
readonly [member: string]: never;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Property reads that must NOT throw.
|
|
85
|
+
*
|
|
86
|
+
* Bundlers, test runners, `console.log`, promise adoption and React all probe
|
|
87
|
+
* values with these keys as part of ordinary machinery, never as an intent to
|
|
88
|
+
* use the API. Throwing on `then` in particular would turn `await`-ing
|
|
89
|
+
* anything holding a deferred value into this error at the wrong site, and
|
|
90
|
+
* throwing on `$$typeof` would break React's element check. They read as
|
|
91
|
+
* `undefined`, exactly as they would on a real function.
|
|
92
|
+
*/
|
|
93
|
+
const INERT_PROPERTIES: ReadonlySet<string> = new Set([
|
|
94
|
+
'$$typeof',
|
|
95
|
+
'__esModule',
|
|
96
|
+
'constructor',
|
|
97
|
+
'displayName',
|
|
98
|
+
'inspect',
|
|
99
|
+
'length',
|
|
100
|
+
'name',
|
|
101
|
+
'nodeType',
|
|
102
|
+
'prototype',
|
|
103
|
+
'then',
|
|
104
|
+
'toJSON',
|
|
105
|
+
'toString',
|
|
106
|
+
'valueOf',
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build one deferred export: a function that throws when called or
|
|
111
|
+
* constructed, wrapped in a proxy so member reads (the enum-constant shape)
|
|
112
|
+
* throw the same error.
|
|
113
|
+
*/
|
|
114
|
+
function deferredSymbol(subpath: string, symbol: string): DeferredApi {
|
|
115
|
+
const throwing = function deferred(): never {
|
|
116
|
+
throw new PyricDeferredApiError(subpath, symbol);
|
|
117
|
+
};
|
|
118
|
+
Object.defineProperty(throwing, 'name', { value: symbol, configurable: true });
|
|
119
|
+
|
|
120
|
+
return new Proxy(throwing, {
|
|
121
|
+
get(target, property, receiver) {
|
|
122
|
+
// Symbol-keyed reads are always machinery (Symbol.toPrimitive,
|
|
123
|
+
// Symbol.hasInstance, Symbol.toStringTag, the inspect hook, …) — never a
|
|
124
|
+
// user reaching for an API member.
|
|
125
|
+
if (typeof property === 'symbol' || INERT_PROPERTIES.has(property)) {
|
|
126
|
+
return Reflect.get(target, property, receiver);
|
|
127
|
+
}
|
|
128
|
+
throw new PyricDeferredApiError(subpath, `${symbol}.${property}`);
|
|
129
|
+
},
|
|
130
|
+
construct() {
|
|
131
|
+
throw new PyricDeferredApiError(subpath, symbol);
|
|
132
|
+
},
|
|
133
|
+
}) as unknown as DeferredApi;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The lazily-materialised export bag for one deferred subpath.
|
|
138
|
+
*
|
|
139
|
+
* Each entry module destructures the symbols it needs off this object:
|
|
140
|
+
*
|
|
141
|
+
* ```ts
|
|
142
|
+
* export const { getFunctions, httpsCallable } = deferredEntry('functions');
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* Destructuring is what makes the names real ESM exports (and what keeps the
|
|
146
|
+
* generated `.d.ts` honest); the proxy just mints a correctly-attributed stub
|
|
147
|
+
* for whichever name is read.
|
|
148
|
+
*
|
|
149
|
+
* @param subpath - The Firebase subpath, without the `firebase/` prefix.
|
|
150
|
+
*/
|
|
151
|
+
export function deferredEntry(subpath: string): Record<string, DeferredApi> {
|
|
152
|
+
const minted = new Map<string, DeferredApi>();
|
|
153
|
+
return new Proxy(Object.create(null) as Record<string, DeferredApi>, {
|
|
154
|
+
get(_target, property) {
|
|
155
|
+
if (typeof property === 'symbol') return undefined as unknown as DeferredApi;
|
|
156
|
+
let api = minted.get(property);
|
|
157
|
+
if (!api) {
|
|
158
|
+
api = deferredSymbol(subpath, property);
|
|
159
|
+
minted.set(property, api);
|
|
160
|
+
}
|
|
161
|
+
return api;
|
|
162
|
+
},
|
|
163
|
+
has() {
|
|
164
|
+
return true;
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|