pyric 0.1.0-alpha.19 → 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.
Files changed (281) hide show
  1. package/README.md +64 -89
  2. package/dist/ai/blocked.d.ts +52 -0
  3. package/dist/ai/blocked.d.ts.map +1 -0
  4. package/dist/ai/blocked.js +83 -0
  5. package/dist/ai/blocked.js.map +1 -0
  6. package/dist/ai/broker/broker.d.ts +50 -0
  7. package/dist/ai/broker/broker.d.ts.map +1 -1
  8. package/dist/ai/broker/broker.js +157 -21
  9. package/dist/ai/broker/broker.js.map +1 -1
  10. package/dist/ai/broker/gemini-engine.d.ts +56 -0
  11. package/dist/ai/broker/gemini-engine.d.ts.map +1 -0
  12. package/dist/ai/broker/gemini-engine.js +235 -0
  13. package/dist/ai/broker/gemini-engine.js.map +1 -0
  14. package/dist/ai/broker/index.d.ts +2 -1
  15. package/dist/ai/broker/index.d.ts.map +1 -1
  16. package/dist/ai/broker/index.js +2 -1
  17. package/dist/ai/broker/index.js.map +1 -1
  18. package/dist/ai/broker/openai-engine.d.ts +10 -1
  19. package/dist/ai/broker/openai-engine.d.ts.map +1 -1
  20. package/dist/ai/broker/openai-engine.js +26 -4
  21. package/dist/ai/broker/openai-engine.js.map +1 -1
  22. package/dist/ai/broker/synthesizer.d.ts +14 -0
  23. package/dist/ai/broker/synthesizer.d.ts.map +1 -1
  24. package/dist/ai/broker/synthesizer.js +31 -0
  25. package/dist/ai/broker/synthesizer.js.map +1 -1
  26. package/dist/ai/broker/types.d.ts +29 -0
  27. package/dist/ai/broker/types.d.ts.map +1 -1
  28. package/dist/ai/instances.js +1 -1
  29. package/dist/ai/instances.js.map +1 -1
  30. package/dist/ai/internal.d.ts +5 -0
  31. package/dist/ai/internal.d.ts.map +1 -1
  32. package/dist/ai/internal.js +5 -0
  33. package/dist/ai/internal.js.map +1 -1
  34. package/dist/ai/response-helpers.d.ts.map +1 -1
  35. package/dist/ai/response-helpers.js +6 -24
  36. package/dist/ai/response-helpers.js.map +1 -1
  37. package/dist/analytics/index.d.ts +40 -0
  38. package/dist/analytics/index.d.ts.map +1 -0
  39. package/dist/analytics/index.js +25 -0
  40. package/dist/analytics/index.js.map +1 -0
  41. package/dist/app-check/index.d.ts +31 -0
  42. package/dist/app-check/index.d.ts.map +1 -0
  43. package/dist/app-check/index.js +20 -0
  44. package/dist/app-check/index.js.map +1 -0
  45. package/dist/auth/instances.js +1 -1
  46. package/dist/auth/instances.js.map +1 -1
  47. package/dist/database/controls.d.ts +1 -1
  48. package/dist/database/controls.js +1 -1
  49. package/dist/database/instances.js +1 -1
  50. package/dist/database/instances.js.map +1 -1
  51. package/dist/database/sandbox/backend.d.ts +1 -0
  52. package/dist/database/sandbox/backend.d.ts.map +1 -1
  53. package/dist/database/sandbox/backend.js +1 -0
  54. package/dist/database/sandbox/backend.js.map +1 -1
  55. package/dist/database/sandbox/rules-eval.d.ts +6 -2
  56. package/dist/database/sandbox/rules-eval.d.ts.map +1 -1
  57. package/dist/database/sandbox/rules-eval.js +15 -2
  58. package/dist/database/sandbox/rules-eval.js.map +1 -1
  59. package/dist/database/sandbox/write-plane.d.ts +1 -0
  60. package/dist/database/sandbox/write-plane.d.ts.map +1 -1
  61. package/dist/database/sandbox/write-plane.js +7 -2
  62. package/dist/database/sandbox/write-plane.js.map +1 -1
  63. package/dist/database/sandbox-controls.d.ts +12 -5
  64. package/dist/database/sandbox-controls.d.ts.map +1 -1
  65. package/dist/database/sandbox-controls.js +20 -9
  66. package/dist/database/sandbox-controls.js.map +1 -1
  67. package/dist/database/sandbox-namespace.d.ts +19 -1
  68. package/dist/database/sandbox-namespace.d.ts.map +1 -1
  69. package/dist/database/sandbox-namespace.js +24 -1
  70. package/dist/database/sandbox-namespace.js.map +1 -1
  71. package/dist/deferred/entry.d.ts +75 -0
  72. package/dist/deferred/entry.d.ts.map +1 -0
  73. package/dist/deferred/entry.js +143 -0
  74. package/dist/deferred/entry.js.map +1 -0
  75. package/dist/firestore/instances.js +1 -1
  76. package/dist/firestore/instances.js.map +1 -1
  77. package/dist/firestore/lite.d.ts +72 -0
  78. package/dist/firestore/lite.d.ts.map +1 -0
  79. package/dist/firestore/lite.js +22 -0
  80. package/dist/firestore/lite.js.map +1 -0
  81. package/dist/firestore/persistence.d.ts +2 -2
  82. package/dist/firestore/persistence.js +1 -1
  83. package/dist/firestore/sandbox-controls.d.ts +2 -0
  84. package/dist/firestore/sandbox-controls.d.ts.map +1 -1
  85. package/dist/firestore/sandbox-controls.js +8 -0
  86. package/dist/firestore/sandbox-controls.js.map +1 -1
  87. package/dist/functions/index.d.ts +29 -0
  88. package/dist/functions/index.d.ts.map +1 -0
  89. package/dist/functions/index.js +20 -0
  90. package/dist/functions/index.js.map +1 -0
  91. package/dist/messaging/client.d.ts +2 -1
  92. package/dist/messaging/client.d.ts.map +1 -1
  93. package/dist/messaging/client.js +2 -2
  94. package/dist/messaging/client.js.map +1 -1
  95. package/dist/messaging/instance.d.ts +2 -2
  96. package/dist/messaging/instance.d.ts.map +1 -1
  97. package/dist/messaging/instance.js +40 -6
  98. package/dist/messaging/instance.js.map +1 -1
  99. package/dist/messaging/sw.d.ts +2 -1
  100. package/dist/messaging/sw.d.ts.map +1 -1
  101. package/dist/messaging/sw.js +2 -2
  102. package/dist/messaging/sw.js.map +1 -1
  103. package/dist/performance/index.d.ts +22 -0
  104. package/dist/performance/index.d.ts.map +1 -0
  105. package/dist/performance/index.js +19 -0
  106. package/dist/performance/index.js.map +1 -0
  107. package/dist/remote-config/index.d.ts +40 -0
  108. package/dist/remote-config/index.d.ts.map +1 -0
  109. package/dist/remote-config/index.js +25 -0
  110. package/dist/remote-config/index.js.map +1 -0
  111. package/dist/rules/grammar/FirestoreValidator.d.ts.map +1 -1
  112. package/dist/rules/grammar/FirestoreValidator.js +43 -27
  113. package/dist/rules/grammar/FirestoreValidator.js.map +1 -1
  114. package/dist/rules/grammar/document-access-count.d.ts +24 -0
  115. package/dist/rules/grammar/document-access-count.d.ts.map +1 -0
  116. package/dist/rules/grammar/document-access-count.js +100 -0
  117. package/dist/rules/grammar/document-access-count.js.map +1 -0
  118. package/dist/rules/linter/ast-utils.d.ts +2 -6
  119. package/dist/rules/linter/ast-utils.d.ts.map +1 -1
  120. package/dist/rules/linter/ast-utils.js +2 -63
  121. package/dist/rules/linter/ast-utils.js.map +1 -1
  122. package/dist/rules/linter/hallucinations.d.ts +8 -0
  123. package/dist/rules/linter/hallucinations.d.ts.map +1 -1
  124. package/dist/rules/linter/hallucinations.js +121 -31
  125. package/dist/rules/linter/hallucinations.js.map +1 -1
  126. package/dist/rules/linter/linter.d.ts +10 -1
  127. package/dist/rules/linter/linter.d.ts.map +1 -1
  128. package/dist/rules/linter/linter.js +23 -9
  129. package/dist/rules/linter/linter.js.map +1 -1
  130. package/dist/rules/rtdb/grammar/RtdbExpr.ohm +5 -2
  131. package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.d.ts +1 -1
  132. package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.d.ts.map +1 -1
  133. package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.js +5 -2
  134. package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.js.map +1 -1
  135. package/dist/rules/rtdb/grammar/simulator.d.ts.map +1 -1
  136. package/dist/rules/rtdb/grammar/simulator.js +61 -15
  137. package/dist/rules/rtdb/grammar/simulator.js.map +1 -1
  138. package/dist/rules/rtdb/simulation/handler.d.ts.map +1 -1
  139. package/dist/rules/rtdb/simulation/handler.js +96 -35
  140. package/dist/rules/rtdb/simulation/handler.js.map +1 -1
  141. package/dist/rules/simulator/document-lookups.d.ts +4 -0
  142. package/dist/rules/simulator/document-lookups.d.ts.map +1 -1
  143. package/dist/rules/simulator/document-lookups.js +42 -3
  144. package/dist/rules/simulator/document-lookups.js.map +1 -1
  145. package/dist/rules/simulator/eval-error.d.ts +11 -0
  146. package/dist/rules/simulator/eval-error.d.ts.map +1 -1
  147. package/dist/rules/simulator/eval-error.js +14 -0
  148. package/dist/rules/simulator/eval-error.js.map +1 -1
  149. package/dist/rules/simulator/evaluation-builtins.d.ts.map +1 -1
  150. package/dist/rules/simulator/evaluation-builtins.js +33 -8
  151. package/dist/rules/simulator/evaluation-builtins.js.map +1 -1
  152. package/dist/rules/simulator/evaluation-context.d.ts +12 -0
  153. package/dist/rules/simulator/evaluation-context.d.ts.map +1 -1
  154. package/dist/rules/simulator/evaluator.d.ts +1 -1
  155. package/dist/rules/simulator/evaluator.d.ts.map +1 -1
  156. package/dist/rules/simulator/evaluator.js +16 -4
  157. package/dist/rules/simulator/evaluator.js.map +1 -1
  158. package/dist/rules/simulator/handler.d.ts.map +1 -1
  159. package/dist/rules/simulator/handler.js +52 -5
  160. package/dist/rules/simulator/handler.js.map +1 -1
  161. package/dist/rules/simulator/lookup-budget.d.ts +37 -0
  162. package/dist/rules/simulator/lookup-budget.d.ts.map +1 -0
  163. package/dist/rules/simulator/lookup-budget.js +96 -0
  164. package/dist/rules/simulator/lookup-budget.js.map +1 -0
  165. package/dist/rules/stdlib-modules.d.ts +7 -0
  166. package/dist/rules/stdlib-modules.d.ts.map +1 -1
  167. package/dist/rules/stdlib-modules.js +11 -11
  168. package/dist/rules/stdlib-modules.js.map +1 -1
  169. package/dist/sandbox/admin-firestore/get-firestore.d.ts +6 -0
  170. package/dist/sandbox/admin-firestore/get-firestore.d.ts.map +1 -1
  171. package/dist/sandbox/admin-firestore/get-firestore.js +22 -3
  172. package/dist/sandbox/admin-firestore/get-firestore.js.map +1 -1
  173. package/dist/sandbox/index.d.ts +1 -1
  174. package/dist/sandbox/index.d.ts.map +1 -1
  175. package/dist/sandbox/index.js +1 -1
  176. package/dist/sandbox/index.js.map +1 -1
  177. package/dist/sandbox/persistence/types.d.ts +1 -1
  178. package/dist/sandbox/remote.d.ts +2 -2
  179. package/dist/sandbox/sandbox-context.d.ts +8 -1
  180. package/dist/sandbox/sandbox-context.d.ts.map +1 -1
  181. package/dist/sandbox/sandbox-context.js +59 -8
  182. package/dist/sandbox/sandbox-context.js.map +1 -1
  183. package/dist/sandbox/types/auth-state.d.ts +1 -0
  184. package/dist/sandbox/types/auth-state.d.ts.map +1 -1
  185. package/dist/sandbox/types/events.d.ts +2 -1
  186. package/dist/sandbox/types/events.d.ts.map +1 -1
  187. package/dist/sandbox/types/operation.d.ts +1 -0
  188. package/dist/sandbox/types/operation.d.ts.map +1 -1
  189. package/dist/storage/enforce.d.ts +20 -1
  190. package/dist/storage/enforce.d.ts.map +1 -1
  191. package/dist/storage/enforce.js +47 -14
  192. package/dist/storage/enforce.js.map +1 -1
  193. package/dist/storage/index.d.ts +1 -1
  194. package/dist/storage/index.d.ts.map +1 -1
  195. package/dist/storage/list.js +1 -1
  196. package/dist/storage/list.js.map +1 -1
  197. package/dist/storage/persistence.d.ts.map +1 -1
  198. package/dist/storage/persistence.js +66 -14
  199. package/dist/storage/persistence.js.map +1 -1
  200. package/dist/storage/sandbox/rules-evaluation-error.d.ts +25 -0
  201. package/dist/storage/sandbox/rules-evaluation-error.d.ts.map +1 -1
  202. package/dist/storage/sandbox/rules-evaluation-error.js +29 -0
  203. package/dist/storage/sandbox/rules-evaluation-error.js.map +1 -1
  204. package/dist/storage/sandbox/rules-evaluator.d.ts +10 -5
  205. package/dist/storage/sandbox/rules-evaluator.d.ts.map +1 -1
  206. package/dist/storage/sandbox/rules-evaluator.js +105 -33
  207. package/dist/storage/sandbox/rules-evaluator.js.map +1 -1
  208. package/dist/storage/sandbox/rules-methods.d.ts.map +1 -1
  209. package/dist/storage/sandbox/rules-methods.js +17 -7
  210. package/dist/storage/sandbox/rules-methods.js.map +1 -1
  211. package/dist/storage/service.d.ts +23 -2
  212. package/dist/storage/service.d.ts.map +1 -1
  213. package/dist/storage/service.js +37 -26
  214. package/dist/storage/service.js.map +1 -1
  215. package/package.json +42 -2
  216. package/src/ai/blocked.ts +97 -0
  217. package/src/ai/broker/broker.ts +153 -22
  218. package/src/ai/broker/gemini-engine.ts +311 -0
  219. package/src/ai/broker/index.ts +2 -0
  220. package/src/ai/broker/openai-engine.ts +31 -3
  221. package/src/ai/broker/synthesizer.ts +34 -0
  222. package/src/ai/broker/types.ts +27 -1
  223. package/src/ai/instances.ts +1 -1
  224. package/src/ai/internal.ts +5 -0
  225. package/src/ai/response-helpers.ts +6 -25
  226. package/src/analytics/index.ts +53 -0
  227. package/src/app/dispatch.test.ts +3 -1
  228. package/src/app-check/index.ts +41 -0
  229. package/src/auth/instances.ts +1 -1
  230. package/src/database/controls.ts +1 -1
  231. package/src/database/instances.ts +1 -1
  232. package/src/database/sandbox/backend.ts +1 -0
  233. package/src/database/sandbox/rules-eval.ts +18 -2
  234. package/src/database/sandbox/write-plane.ts +8 -2
  235. package/src/database/sandbox-controls.ts +29 -9
  236. package/src/database/sandbox-namespace.ts +28 -1
  237. package/src/deferred/entry.ts +167 -0
  238. package/src/firestore/instances.ts +1 -1
  239. package/src/firestore/lite.ts +91 -0
  240. package/src/firestore/persistence.ts +2 -2
  241. package/src/firestore/sandbox-controls.ts +10 -0
  242. package/src/functions/index.ts +38 -0
  243. package/src/messaging/client.ts +3 -2
  244. package/src/messaging/instance.ts +44 -7
  245. package/src/messaging/sw.ts +3 -2
  246. package/src/performance/index.ts +31 -0
  247. package/src/remote-config/index.ts +53 -0
  248. package/src/rules/grammar/FirestoreValidator.ts +44 -28
  249. package/src/rules/grammar/document-access-count.ts +75 -0
  250. package/src/rules/linter/ast-utils.ts +2 -36
  251. package/src/rules/linter/hallucinations.ts +123 -25
  252. package/src/rules/linter/linter.ts +32 -10
  253. package/src/rules/rtdb/grammar/RtdbExpr.ohm +5 -2
  254. package/src/rules/rtdb/grammar/RtdbExpr.ohm.generated.ts +5 -2
  255. package/src/rules/rtdb/grammar/simulator.ts +41 -15
  256. package/src/rules/rtdb/simulation/handler.ts +149 -46
  257. package/src/rules/simulator/document-lookups.ts +46 -3
  258. package/src/rules/simulator/eval-error.ts +16 -0
  259. package/src/rules/simulator/evaluation-builtins.ts +35 -8
  260. package/src/rules/simulator/evaluation-context.ts +12 -0
  261. package/src/rules/simulator/evaluator.ts +14 -3
  262. package/src/rules/simulator/handler.ts +68 -4
  263. package/src/rules/simulator/lookup-budget.ts +101 -0
  264. package/src/rules/stdlib-modules.ts +19 -11
  265. package/src/sandbox/admin-firestore/get-firestore.ts +19 -3
  266. package/src/sandbox/index.ts +5 -1
  267. package/src/sandbox/persistence/types.ts +1 -1
  268. package/src/sandbox/remote.ts +2 -2
  269. package/src/sandbox/sandbox-context.ts +69 -9
  270. package/src/sandbox/types/auth-state.ts +1 -1
  271. package/src/sandbox/types/events.ts +2 -1
  272. package/src/sandbox/types/operation.ts +1 -1
  273. package/src/storage/enforce.ts +53 -14
  274. package/src/storage/index.ts +1 -1
  275. package/src/storage/list.ts +1 -1
  276. package/src/storage/persistence.ts +74 -16
  277. package/src/storage/sandbox/rules-evaluation-error.ts +32 -0
  278. package/src/storage/sandbox/rules-evaluator.ts +109 -32
  279. package/src/storage/sandbox/rules-methods.ts +21 -7
  280. package/src/storage/service.ts +93 -27
  281. package/README.md.orig +0 -53
@@ -0,0 +1,311 @@
1
+ /**
2
+ * The Gemini AnswerEngine: Gemini wire in, Google AI Studio / Vertex AI REST API
3
+ * out, Gemini wire back.
4
+ *
5
+ * Unlike the browser-side `@firebase/ai` SDK (which calls Vertex AI directly
6
+ * from the browser and fails with HTTP 401 for sandboxed mock auth users),
7
+ * `GeminiEngine` runs on Pyric's backend server, authenticating via a local
8
+ * API key (`GEMINI_API_KEY` / `GOOGLE_GENAI_API_KEY` / `VITE_GEMINI_API_KEY`)
9
+ * or Google Cloud Application Default Credentials (`gcloud auth application-default login`)
10
+ * without exposing secrets to the browser.
11
+ */
12
+
13
+ import { AiBrokerError, errorEnvelope, redactUrl } from './synthesizer.js';
14
+ import type {
15
+ AnswerEngine,
16
+ CountTokensRequest,
17
+ CountTokensResponse,
18
+ GenerateContentRequest,
19
+ ModelResolution,
20
+ WireChunk,
21
+ WireResponse,
22
+ } from './types.js';
23
+
24
+ /** Where the gemini engine talks when no `baseUrl` overrides it. Published
25
+ * through `pyric/ai/internal` so the CLI's startup banner can name the same
26
+ * endpoint instead of keeping its own copy. */
27
+ export const GEMINI_DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com';
28
+
29
+ /**
30
+ * BROWSER-CLEAN: acquires `node:child_process` lazily via `process.getBuiltinModule`
31
+ * so no static node import exists for browser bundlers to resolve.
32
+ */
33
+ function getExecSync(): ((command: string, options: Record<string, unknown>) => string) | null {
34
+ const get = (globalThis as { process?: { getBuiltinModule?: (id: string) => unknown } }).process
35
+ ?.getBuiltinModule;
36
+ const hasGetBuiltinModule = typeof get === 'function';
37
+ if (!hasGetBuiltinModule) {
38
+ return null;
39
+ }
40
+ const cp = get.call(process, 'node:child_process') as
41
+ | { execSync?: (cmd: string, opts: unknown) => string }
42
+ | undefined;
43
+ const hasExecSync = cp !== undefined && typeof cp.execSync === 'function';
44
+ if (!hasExecSync) {
45
+ return null;
46
+ }
47
+ return cp.execSync as (command: string, options: Record<string, unknown>) => string;
48
+ }
49
+
50
+ export interface GeminiEngineOptions {
51
+ /** Explicit API key. Absent ⇒ resolved from environment variables or ADC. */
52
+ apiKey?: string;
53
+ /** Explicit base URL override (default: https://generativelanguage.googleapis.com). */
54
+ baseUrl?: string;
55
+ /** Explicit fetch seam for testing or custom transport. */
56
+ fetch?: typeof fetch;
57
+ }
58
+
59
+ export class GeminiEngine implements AnswerEngine {
60
+ private readonly baseUrl: string;
61
+ private readonly fetchImpl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
62
+ private readonly explicitKey?: string;
63
+
64
+ constructor(options?: GeminiEngineOptions) {
65
+ this.baseUrl = (options?.baseUrl ?? GEMINI_DEFAULT_BASE_URL).replace(/\/$/, '');
66
+ this.fetchImpl = options?.fetch ?? ((input, init) => fetch(input, init));
67
+ this.explicitKey = options?.apiKey;
68
+ }
69
+
70
+ /**
71
+ * Resolves authentication credentials sequentially, checking static options,
72
+ * environment variables, and finally Google Cloud Application Default
73
+ * Credentials (`gcloud auth application-default print-access-token`).
74
+ */
75
+ private async resolveAuthToken(): Promise<{ token: string; isBearer: boolean }> {
76
+ let key = this.explicitKey;
77
+ if (key === undefined) {
78
+ key = process.env.GEMINI_API_KEY;
79
+ }
80
+ if (key === undefined) {
81
+ key = process.env.GOOGLE_GENAI_API_KEY;
82
+ }
83
+ if (key === undefined) {
84
+ key = process.env.VITE_GEMINI_API_KEY;
85
+ }
86
+
87
+ if (key !== undefined) {
88
+ const trimmedKey = key.trim();
89
+ const hasStaticKey = trimmedKey !== '';
90
+ if (hasStaticKey) {
91
+ return { token: trimmedKey, isBearer: false };
92
+ }
93
+ }
94
+
95
+ try {
96
+ const execSyncFn = getExecSync();
97
+ const hasExecSyncFn = execSyncFn !== null;
98
+ if (hasExecSyncFn) {
99
+ const adcToken = execSyncFn('gcloud auth application-default print-access-token', {
100
+ encoding: 'utf8',
101
+ stdio: ['ignore', 'pipe', 'ignore'],
102
+ }).trim();
103
+ const hasAdcToken = adcToken !== '';
104
+ if (hasAdcToken) {
105
+ return { token: adcToken, isBearer: true };
106
+ }
107
+ }
108
+ } catch {
109
+ // Best-effort attempt to recover ADC credentials when env keys are absent
110
+ }
111
+
112
+ throw new AiBrokerError(
113
+ errorEnvelope(
114
+ 401,
115
+ 'Pyric AI production passthrough mode requires GEMINI_API_KEY, GOOGLE_GENAI_API_KEY, or VITE_GEMINI_API_KEY in your server environment (or Application Default Credentials via `gcloud auth application-default login`).',
116
+ 'UNAUTHENTICATED',
117
+ ),
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Which model this engine ACTUALLY calls upstream, and why it differs.
123
+ * Experimental and superseded flash aliases are redirected onto the served
124
+ * `gemini-flash-lite-latest`; everything else passes through. Reported so
125
+ * the broker can announce the redirect rather than let a developer believe
126
+ * `gemini-2.5-flash` answered when `gemini-flash-lite-latest` did
127
+ * ({@link AnswerEngine.resolveEffectiveModel}).
128
+ */
129
+ resolveEffectiveModel(model: string): ModelResolution {
130
+ const stripped = model.replace(/^models\//, '');
131
+ const isExperimentalFlashLite =
132
+ stripped === 'gemini-3.5-flash-lite' ||
133
+ stripped === 'gemini-2.5-flash' ||
134
+ stripped === 'gemini-2.5-flash-lite' ||
135
+ stripped === 'gemini-1.5-flash';
136
+ if (isExperimentalFlashLite) {
137
+ return { model: 'gemini-flash-lite-latest', reason: 'experimental alias' };
138
+ }
139
+ return { model: stripped, reason: 'passthrough' };
140
+ }
141
+
142
+ private normalizeModel(model: string): string {
143
+ return `models/${this.resolveEffectiveModel(model).model}`;
144
+ }
145
+
146
+ /**
147
+ * Formats upstream REST endpoint URLs and request headers for either Google
148
+ * AI Studio (`generativelanguage.googleapis.com`) or Vertex AI regional APIs.
149
+ */
150
+ private formatUpstreamRequest(
151
+ resource: string,
152
+ action: string,
153
+ auth: { token: string; isBearer: boolean },
154
+ ): { url: string; headers: Record<string, string> } {
155
+ const isVertexUrl =
156
+ this.baseUrl.includes('firebasevertexai.googleapis.com') ||
157
+ this.baseUrl.includes('aiplatform.googleapis.com');
158
+ const shouldUseBearerAuth = auth.isBearer || isVertexUrl;
159
+
160
+ const headers: Record<string, string> = {
161
+ 'Content-Type': 'application/json',
162
+ };
163
+ if (shouldUseBearerAuth) {
164
+ headers['Authorization'] = `Bearer ${auth.token}`;
165
+ return {
166
+ url: `${this.baseUrl}/v1beta/${resource}:${action}`,
167
+ headers,
168
+ };
169
+ }
170
+
171
+ return {
172
+ url: `${this.baseUrl}/v1beta/${resource}:${action}?key=${encodeURIComponent(auth.token)}`,
173
+ headers,
174
+ };
175
+ }
176
+
177
+ private async fetchUpstream(
178
+ url: string,
179
+ headers: Record<string, string>,
180
+ body: unknown,
181
+ ): Promise<Response> {
182
+ let response: Response;
183
+ try {
184
+ response = await this.fetchImpl(url, {
185
+ method: 'POST',
186
+ headers,
187
+ body: JSON.stringify(body),
188
+ });
189
+ } catch (err) {
190
+ // Redact both: `url` always carries the key when auth is a static key
191
+ // (not bearer/ADC); `message` is defensively redacted too in case the
192
+ // fetch implementation's own error text echoes the request URL.
193
+ const rawMessage = err instanceof Error ? err.message : String(err);
194
+ const safeUrl = redactUrl(url);
195
+ const safeMessage = redactUrl(rawMessage);
196
+ throw new AiBrokerError(
197
+ errorEnvelope(
198
+ 502,
199
+ `Failed to connect to Gemini API at ${safeUrl}: ${safeMessage}`,
200
+ 'UNAVAILABLE',
201
+ ),
202
+ );
203
+ }
204
+
205
+ const isSuccess = response.ok;
206
+ if (!isSuccess) {
207
+ const errorText = await response.text().catch(() => '');
208
+ throw new AiBrokerError(
209
+ errorEnvelope(
210
+ response.status,
211
+ `Gemini API returned status ${response.status}: ${redactUrl(errorText)}`,
212
+ 'INTERNAL',
213
+ ),
214
+ );
215
+ }
216
+
217
+ return response;
218
+ }
219
+
220
+ async generateContent(req: GenerateContentRequest, model: string): Promise<WireResponse> {
221
+ const auth = await this.resolveAuthToken();
222
+ const resource = this.normalizeModel(model);
223
+ const { url, headers } = this.formatUpstreamRequest(resource, 'generateContent', auth);
224
+ const response = await this.fetchUpstream(url, headers, req);
225
+ return (await response.json()) as WireResponse;
226
+ }
227
+
228
+ async *streamGenerateContent(
229
+ req: GenerateContentRequest,
230
+ model: string,
231
+ ): AsyncIterable<WireChunk> {
232
+ const auth = await this.resolveAuthToken();
233
+ const resource = this.normalizeModel(model);
234
+ const { url, headers } = this.formatUpstreamRequest(
235
+ resource,
236
+ 'streamGenerateContent?alt=sse',
237
+ auth,
238
+ );
239
+ const response = await this.fetchUpstream(url, headers, req);
240
+
241
+ const hasBody = response.body !== null && response.body !== undefined;
242
+ if (!hasBody) {
243
+ throw new AiBrokerError(
244
+ errorEnvelope(500, 'Gemini API returned an empty stream response.', 'INTERNAL'),
245
+ );
246
+ }
247
+
248
+ const reader = response.body.getReader();
249
+ const decoder = new TextDecoder();
250
+ let buffer = '';
251
+
252
+ try {
253
+ while (true) {
254
+ const { done, value } = await reader.read();
255
+ const isDone = done === true;
256
+ if (isDone) {
257
+ break;
258
+ }
259
+ buffer += decoder.decode(value, { stream: true });
260
+ const lines = buffer.split(/\r?\n/);
261
+ buffer = lines.pop() ?? '';
262
+
263
+ for (const line of lines) {
264
+ const isDataLine = line.startsWith('data: ');
265
+ if (!isDataLine) {
266
+ continue;
267
+ }
268
+ const dataStr = line.slice(6).trim();
269
+ const isEmptyOrDone = dataStr === '' || dataStr === '[DONE]';
270
+ if (isEmptyOrDone) {
271
+ continue;
272
+ }
273
+ try {
274
+ const chunk = JSON.parse(dataStr) as WireChunk;
275
+ yield chunk;
276
+ } catch {
277
+ // Ignore malformed SSE chunk in stream
278
+ }
279
+ }
280
+ }
281
+
282
+ const hasTrailingBuffer = buffer.trim().length > 0;
283
+ if (hasTrailingBuffer) {
284
+ const line = buffer.trim();
285
+ const isDataLine = line.startsWith('data: ');
286
+ if (isDataLine) {
287
+ const dataStr = line.slice(6).trim();
288
+ const isNotEmptyOrDone = dataStr !== '' && dataStr !== '[DONE]';
289
+ if (isNotEmptyOrDone) {
290
+ try {
291
+ const chunk = JSON.parse(dataStr) as WireChunk;
292
+ yield chunk;
293
+ } catch {
294
+ // Ignore malformed SSE chunk in stream
295
+ }
296
+ }
297
+ }
298
+ }
299
+ } finally {
300
+ reader.releaseLock();
301
+ }
302
+ }
303
+
304
+ async countTokens(req: CountTokensRequest, model: string): Promise<CountTokensResponse> {
305
+ const auth = await this.resolveAuthToken();
306
+ const resource = this.normalizeModel(model);
307
+ const { url, headers } = this.formatUpstreamRequest(resource, 'countTokens', auth);
308
+ const response = await this.fetchUpstream(url, headers, req);
309
+ return (await response.json()) as CountTokensResponse;
310
+ }
311
+ }
@@ -7,6 +7,7 @@
7
7
 
8
8
  export { AiBroker, loadObservationEnvelope, type AiBrokerOptions } from './broker.js';
9
9
  export { ScriptedEngine, lastUserText, promptTextOf } from './scripted-engine.js';
10
+ export { GeminiEngine, GEMINI_DEFAULT_BASE_URL, type GeminiEngineOptions } from './gemini-engine.js';
10
11
  export {
11
12
  OpenAiEngine,
12
13
  ToolCallBuffer,
@@ -36,6 +37,7 @@ export {
36
37
  estimateTokens,
37
38
  mintThoughtSignature,
38
39
  mintFunctionCallId,
40
+ redactUrl,
39
41
  type SynthesizeOptions,
40
42
  } from './synthesizer.js';
41
43
  export type {
@@ -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
- const bare = model.startsWith('models/') ? model.slice('models/'.length) : model;
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(502, `openai engine: upstream fetch failed: ${String(err)}`, 'UNAVAILABLE'),
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(
@@ -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 ──────────────────────────────────────────────────────
@@ -191,4 +216,5 @@ export type EngineConfig =
191
216
  modelMap?: Record<string, string>;
192
217
  /** Injectable fetch for tests; defaults to globalThis.fetch. */
193
218
  fetch?: typeof fetch;
194
- };
219
+ }
220
+ | { kind: 'gemini'; baseUrl?: string; apiKey?: string; fetch?: typeof fetch };
@@ -91,7 +91,7 @@ export function getAI(target?: Sandbox | FirebaseApp, options?: AIOptions): AI {
91
91
  return sandboxAI(target, options);
92
92
  }
93
93
  throw new TypeError(
94
- 'pyric/ai is a sandbox-only mirror. Package resolution must leave firebase/ai unchanged for production; activate pyric dev or @pyric/cli/register before importing to select the sandbox.',
94
+ 'pyric/ai is a sandbox-only mirror. Package resolution must leave firebase/ai unchanged for production; activate pyric sandbox or @pyric/cli/register before importing to select the sandbox.',
95
95
  );
96
96
  }
97
97
 
@@ -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 { FinishReason } from './enums.js';
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
- const badFinishReasons: string[] = [
35
- FinishReason.RECITATION,
36
- FinishReason.SAFETY,
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 (hadBadFinishReason(firstCandidate)) {
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 (hadBadFinishReason(response.candidates[0]!)) {
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;
@@ -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;
@@ -80,7 +80,7 @@ export function getAuth(target?: Sandbox | FirebaseApp): Auth {
80
80
  return handle;
81
81
  }
82
82
  throw new TypeError(
83
- 'pyric/auth is a sandbox-only mirror. Package resolution must leave firebase/auth unchanged for production; activate pyric dev or @pyric/cli/register before importing to select the sandbox.',
83
+ 'pyric/auth is a sandbox-only mirror. Package resolution must leave firebase/auth unchanged for production; activate pyric sandbox or @pyric/cli/register before importing to select the sandbox.',
84
84
  );
85
85
  }
86
86