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.
Files changed (225) 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 +49 -0
  7. package/dist/ai/broker/broker.d.ts.map +1 -1
  8. package/dist/ai/broker/broker.js +90 -0
  9. package/dist/ai/broker/broker.js.map +1 -1
  10. package/dist/ai/broker/gemini-engine.d.ts +14 -1
  11. package/dist/ai/broker/gemini-engine.d.ts.map +1 -1
  12. package/dist/ai/broker/gemini-engine.js +30 -9
  13. package/dist/ai/broker/gemini-engine.js.map +1 -1
  14. package/dist/ai/broker/index.d.ts +2 -2
  15. package/dist/ai/broker/index.d.ts.map +1 -1
  16. package/dist/ai/broker/index.js +2 -2
  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 +24 -0
  27. package/dist/ai/broker/types.d.ts.map +1 -1
  28. package/dist/ai/internal.d.ts +5 -0
  29. package/dist/ai/internal.d.ts.map +1 -1
  30. package/dist/ai/internal.js +5 -0
  31. package/dist/ai/internal.js.map +1 -1
  32. package/dist/ai/response-helpers.d.ts.map +1 -1
  33. package/dist/ai/response-helpers.js +6 -24
  34. package/dist/ai/response-helpers.js.map +1 -1
  35. package/dist/analytics/index.d.ts +40 -0
  36. package/dist/analytics/index.d.ts.map +1 -0
  37. package/dist/analytics/index.js +25 -0
  38. package/dist/analytics/index.js.map +1 -0
  39. package/dist/app-check/index.d.ts +31 -0
  40. package/dist/app-check/index.d.ts.map +1 -0
  41. package/dist/app-check/index.js +20 -0
  42. package/dist/app-check/index.js.map +1 -0
  43. package/dist/database/controls.d.ts +1 -1
  44. package/dist/database/controls.js +1 -1
  45. package/dist/database/sandbox/rules-eval.d.ts +1 -1
  46. package/dist/database/sandbox/rules-eval.d.ts.map +1 -1
  47. package/dist/database/sandbox/rules-eval.js +2 -2
  48. package/dist/database/sandbox/rules-eval.js.map +1 -1
  49. package/dist/database/sandbox/write-plane.js +2 -2
  50. package/dist/database/sandbox/write-plane.js.map +1 -1
  51. package/dist/database/sandbox-controls.d.ts +12 -5
  52. package/dist/database/sandbox-controls.d.ts.map +1 -1
  53. package/dist/database/sandbox-controls.js +20 -9
  54. package/dist/database/sandbox-controls.js.map +1 -1
  55. package/dist/deferred/entry.d.ts +75 -0
  56. package/dist/deferred/entry.d.ts.map +1 -0
  57. package/dist/deferred/entry.js +143 -0
  58. package/dist/deferred/entry.js.map +1 -0
  59. package/dist/firestore/lite.d.ts +72 -0
  60. package/dist/firestore/lite.d.ts.map +1 -0
  61. package/dist/firestore/lite.js +22 -0
  62. package/dist/firestore/lite.js.map +1 -0
  63. package/dist/firestore/persistence.d.ts +2 -2
  64. package/dist/firestore/persistence.js +1 -1
  65. package/dist/functions/index.d.ts +29 -0
  66. package/dist/functions/index.d.ts.map +1 -0
  67. package/dist/functions/index.js +20 -0
  68. package/dist/functions/index.js.map +1 -0
  69. package/dist/performance/index.d.ts +22 -0
  70. package/dist/performance/index.d.ts.map +1 -0
  71. package/dist/performance/index.js +19 -0
  72. package/dist/performance/index.js.map +1 -0
  73. package/dist/remote-config/index.d.ts +40 -0
  74. package/dist/remote-config/index.d.ts.map +1 -0
  75. package/dist/remote-config/index.js +25 -0
  76. package/dist/remote-config/index.js.map +1 -0
  77. package/dist/rules/grammar/FirestoreValidator.d.ts.map +1 -1
  78. package/dist/rules/grammar/FirestoreValidator.js +43 -27
  79. package/dist/rules/grammar/FirestoreValidator.js.map +1 -1
  80. package/dist/rules/grammar/document-access-count.d.ts +24 -0
  81. package/dist/rules/grammar/document-access-count.d.ts.map +1 -0
  82. package/dist/rules/grammar/document-access-count.js +100 -0
  83. package/dist/rules/grammar/document-access-count.js.map +1 -0
  84. package/dist/rules/linter/ast-utils.d.ts +2 -6
  85. package/dist/rules/linter/ast-utils.d.ts.map +1 -1
  86. package/dist/rules/linter/ast-utils.js +2 -63
  87. package/dist/rules/linter/ast-utils.js.map +1 -1
  88. package/dist/rules/linter/hallucinations.d.ts +8 -0
  89. package/dist/rules/linter/hallucinations.d.ts.map +1 -1
  90. package/dist/rules/linter/hallucinations.js +121 -31
  91. package/dist/rules/linter/hallucinations.js.map +1 -1
  92. package/dist/rules/linter/linter.d.ts +10 -1
  93. package/dist/rules/linter/linter.d.ts.map +1 -1
  94. package/dist/rules/linter/linter.js +23 -9
  95. package/dist/rules/linter/linter.js.map +1 -1
  96. package/dist/rules/rtdb/grammar/simulator.d.ts.map +1 -1
  97. package/dist/rules/rtdb/grammar/simulator.js +13 -13
  98. package/dist/rules/rtdb/grammar/simulator.js.map +1 -1
  99. package/dist/rules/rtdb/simulation/handler.d.ts.map +1 -1
  100. package/dist/rules/rtdb/simulation/handler.js +96 -35
  101. package/dist/rules/rtdb/simulation/handler.js.map +1 -1
  102. package/dist/rules/simulator/document-lookups.d.ts +4 -0
  103. package/dist/rules/simulator/document-lookups.d.ts.map +1 -1
  104. package/dist/rules/simulator/document-lookups.js +42 -3
  105. package/dist/rules/simulator/document-lookups.js.map +1 -1
  106. package/dist/rules/simulator/eval-error.d.ts +11 -0
  107. package/dist/rules/simulator/eval-error.d.ts.map +1 -1
  108. package/dist/rules/simulator/eval-error.js +14 -0
  109. package/dist/rules/simulator/eval-error.js.map +1 -1
  110. package/dist/rules/simulator/evaluation-builtins.d.ts.map +1 -1
  111. package/dist/rules/simulator/evaluation-builtins.js +33 -8
  112. package/dist/rules/simulator/evaluation-builtins.js.map +1 -1
  113. package/dist/rules/simulator/evaluation-context.d.ts +12 -0
  114. package/dist/rules/simulator/evaluation-context.d.ts.map +1 -1
  115. package/dist/rules/simulator/evaluator.d.ts +1 -1
  116. package/dist/rules/simulator/evaluator.d.ts.map +1 -1
  117. package/dist/rules/simulator/evaluator.js +16 -4
  118. package/dist/rules/simulator/evaluator.js.map +1 -1
  119. package/dist/rules/simulator/handler.d.ts.map +1 -1
  120. package/dist/rules/simulator/handler.js +52 -5
  121. package/dist/rules/simulator/handler.js.map +1 -1
  122. package/dist/rules/simulator/lookup-budget.d.ts +37 -0
  123. package/dist/rules/simulator/lookup-budget.d.ts.map +1 -0
  124. package/dist/rules/simulator/lookup-budget.js +96 -0
  125. package/dist/rules/simulator/lookup-budget.js.map +1 -0
  126. package/dist/rules/stdlib-modules.d.ts +7 -0
  127. package/dist/rules/stdlib-modules.d.ts.map +1 -1
  128. package/dist/rules/stdlib-modules.js +11 -11
  129. package/dist/rules/stdlib-modules.js.map +1 -1
  130. package/dist/sandbox/admin-firestore/get-firestore.d.ts +6 -0
  131. package/dist/sandbox/admin-firestore/get-firestore.d.ts.map +1 -1
  132. package/dist/sandbox/admin-firestore/get-firestore.js +22 -3
  133. package/dist/sandbox/admin-firestore/get-firestore.js.map +1 -1
  134. package/dist/sandbox/index.d.ts +1 -1
  135. package/dist/sandbox/index.d.ts.map +1 -1
  136. package/dist/sandbox/index.js +1 -1
  137. package/dist/sandbox/index.js.map +1 -1
  138. package/dist/sandbox/persistence/types.d.ts +1 -1
  139. package/dist/sandbox/remote.d.ts +2 -2
  140. package/dist/sandbox/sandbox-context.d.ts +8 -1
  141. package/dist/sandbox/sandbox-context.d.ts.map +1 -1
  142. package/dist/sandbox/sandbox-context.js +59 -8
  143. package/dist/sandbox/sandbox-context.js.map +1 -1
  144. package/dist/sandbox/types/auth-state.d.ts +1 -0
  145. package/dist/sandbox/types/auth-state.d.ts.map +1 -1
  146. package/dist/sandbox/types/events.d.ts +2 -1
  147. package/dist/sandbox/types/events.d.ts.map +1 -1
  148. package/dist/sandbox/types/operation.d.ts +1 -0
  149. package/dist/sandbox/types/operation.d.ts.map +1 -1
  150. package/dist/storage/enforce.d.ts +20 -1
  151. package/dist/storage/enforce.d.ts.map +1 -1
  152. package/dist/storage/enforce.js +47 -14
  153. package/dist/storage/enforce.js.map +1 -1
  154. package/dist/storage/index.d.ts +1 -1
  155. package/dist/storage/index.d.ts.map +1 -1
  156. package/dist/storage/list.js +1 -1
  157. package/dist/storage/list.js.map +1 -1
  158. package/dist/storage/sandbox/rules-evaluation-error.d.ts +25 -0
  159. package/dist/storage/sandbox/rules-evaluation-error.d.ts.map +1 -1
  160. package/dist/storage/sandbox/rules-evaluation-error.js +29 -0
  161. package/dist/storage/sandbox/rules-evaluation-error.js.map +1 -1
  162. package/dist/storage/sandbox/rules-evaluator.d.ts +10 -5
  163. package/dist/storage/sandbox/rules-evaluator.d.ts.map +1 -1
  164. package/dist/storage/sandbox/rules-evaluator.js +105 -33
  165. package/dist/storage/sandbox/rules-evaluator.js.map +1 -1
  166. package/dist/storage/sandbox/rules-methods.d.ts.map +1 -1
  167. package/dist/storage/sandbox/rules-methods.js +17 -7
  168. package/dist/storage/sandbox/rules-methods.js.map +1 -1
  169. package/dist/storage/service.d.ts +23 -2
  170. package/dist/storage/service.d.ts.map +1 -1
  171. package/dist/storage/service.js +36 -26
  172. package/dist/storage/service.js.map +1 -1
  173. package/package.json +41 -1
  174. package/src/ai/blocked.ts +97 -0
  175. package/src/ai/broker/broker.ts +88 -0
  176. package/src/ai/broker/gemini-engine.ts +36 -9
  177. package/src/ai/broker/index.ts +2 -1
  178. package/src/ai/broker/openai-engine.ts +31 -3
  179. package/src/ai/broker/synthesizer.ts +34 -0
  180. package/src/ai/broker/types.ts +25 -0
  181. package/src/ai/internal.ts +5 -0
  182. package/src/ai/response-helpers.ts +6 -25
  183. package/src/analytics/index.ts +53 -0
  184. package/src/app/dispatch.test.ts +3 -1
  185. package/src/app-check/index.ts +41 -0
  186. package/src/database/controls.ts +1 -1
  187. package/src/database/sandbox/rules-eval.ts +2 -2
  188. package/src/database/sandbox/write-plane.ts +2 -2
  189. package/src/database/sandbox-controls.ts +29 -9
  190. package/src/deferred/entry.ts +167 -0
  191. package/src/firestore/lite.ts +91 -0
  192. package/src/firestore/persistence.ts +2 -2
  193. package/src/functions/index.ts +38 -0
  194. package/src/performance/index.ts +31 -0
  195. package/src/remote-config/index.ts +53 -0
  196. package/src/rules/grammar/FirestoreValidator.ts +44 -28
  197. package/src/rules/grammar/document-access-count.ts +75 -0
  198. package/src/rules/linter/ast-utils.ts +2 -36
  199. package/src/rules/linter/hallucinations.ts +123 -25
  200. package/src/rules/linter/linter.ts +32 -10
  201. package/src/rules/rtdb/grammar/simulator.ts +12 -13
  202. package/src/rules/rtdb/simulation/handler.ts +149 -46
  203. package/src/rules/simulator/document-lookups.ts +46 -3
  204. package/src/rules/simulator/eval-error.ts +16 -0
  205. package/src/rules/simulator/evaluation-builtins.ts +35 -8
  206. package/src/rules/simulator/evaluation-context.ts +12 -0
  207. package/src/rules/simulator/evaluator.ts +14 -3
  208. package/src/rules/simulator/handler.ts +68 -4
  209. package/src/rules/simulator/lookup-budget.ts +101 -0
  210. package/src/rules/stdlib-modules.ts +19 -11
  211. package/src/sandbox/admin-firestore/get-firestore.ts +19 -3
  212. package/src/sandbox/index.ts +5 -1
  213. package/src/sandbox/persistence/types.ts +1 -1
  214. package/src/sandbox/remote.ts +2 -2
  215. package/src/sandbox/sandbox-context.ts +69 -9
  216. package/src/sandbox/types/auth-state.ts +1 -1
  217. package/src/sandbox/types/events.ts +2 -1
  218. package/src/sandbox/types/operation.ts +1 -1
  219. package/src/storage/enforce.ts +53 -14
  220. package/src/storage/index.ts +1 -1
  221. package/src/storage/list.ts +1 -1
  222. package/src/storage/sandbox/rules-evaluation-error.ts +32 -0
  223. package/src/storage/sandbox/rules-evaluator.ts +109 -32
  224. package/src/storage/sandbox/rules-methods.ts +21 -7
  225. 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
- 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 ──────────────────────────────────────────────────────
@@ -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;
@@ -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 dev`'s own flags — matching `pyric/firestore`'s
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 = 'allow';
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-allow when no rules are loaded.
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, ...(updates.length > 1 ? { updates } : {}),
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, ...(multiPath ? { updates } : {}),
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
- /** Replace the active RTDB rules. Pass `null` to restore default allow. */
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
- sandbox: LocalSandbox,
31
+ target: RtdbTarget,
12
32
  rules: RtdbRulesJson | null,
13
33
  ): void {
14
- getOrCreateBackend(sandbox).setRules(rules);
34
+ backendOf(target).setRules(rules);
15
35
  }
16
36
 
17
37
  /** Read the currently active rules as detached JSON. */
18
- export function getActiveRules(sandbox: LocalSandbox): RtdbRulesJson | null {
19
- return getOrCreateBackend(sandbox).getActiveRules();
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
- sandbox: LocalSandbox,
44
+ target: RtdbTarget,
25
45
  data: Record<string, unknown>,
26
46
  ): void {
27
- getOrCreateBackend(sandbox).setData(data as Record<string, JsonValue>);
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(sandbox: LocalSandbox): JsonValue {
32
- return getOrCreateBackend(sandbox).snapshotState();
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
+ }