@warmdrift/kgauto-compiler 2.0.0-alpha.23 → 2.0.0-alpha.25

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.
@@ -2,6 +2,41 @@ import { G as GlassboxEvent } from '../types-D9WndxeD.mjs';
2
2
  import '../ir-B9zqlwjH.mjs';
3
3
  import '../dialect.mjs';
4
4
 
5
+ /**
6
+ * Internal config + hook types for createGlassboxRoutes().
7
+ *
8
+ * The public contract lives on `GlassboxRoutesConfig` in ./index.ts; these
9
+ * are the narrower per-handler shapes consumed by proxy.ts and stream.ts.
10
+ */
11
+
12
+ /**
13
+ * Wire contract for the Glass-Box Chrome extension's brain-poll endpoint.
14
+ *
15
+ * The list mode of `proxy(req)` returns `{ traces: TraceSummary[] }`. The
16
+ * detail mode (`?traceId=<id>`) returns a single `TraceDetail`. These are
17
+ * the camelCase shapes the extension renderer expects — distinct from the
18
+ * snake_case `compile_outcomes` row shape that PostgREST returns. The
19
+ * factory's typed `rowToSummary` / `rowToDetail` transformer is the single
20
+ * canonical boundary between the DB shape and the wire shape (see
21
+ * `feedback_typed_boundary_transformers.md` in kgauto memory for the rule).
22
+ */
23
+ interface TraceSummary {
24
+ traceId: string;
25
+ appId: string;
26
+ archetype: string;
27
+ target: string;
28
+ createdAt: string;
29
+ tokensIn: number;
30
+ tokensOut: number;
31
+ estimatedCostUsd: number;
32
+ }
33
+ interface TraceDetail extends TraceSummary {
34
+ mutationsApplied: unknown[];
35
+ advisories: unknown[];
36
+ rawRequest?: unknown;
37
+ rawResponse?: unknown;
38
+ }
39
+
5
40
  /**
6
41
  * Public entry point for `@warmdrift/kgauto-compiler/glassbox-routes`.
7
42
  *
@@ -12,11 +47,12 @@ import '../dialect.mjs';
12
47
  * // app/api/glassbox/proxy/route.ts
13
48
  * import { createGlassboxRoutes } from '@warmdrift/kgauto-compiler/glassbox-routes';
14
49
  * const { proxy } = createGlassboxRoutes({
15
- * installToken: process.env.GLASSBOX_INSTALL_TOKEN!,
16
- * extensionId: process.env.GLASSBOX_EXTENSION_ID!,
50
+ * installToken: process.env.GLASSBOX_INSTALL_TOKEN!,
51
+ * extensionId: process.env.GLASSBOX_EXTENSION_ID!,
17
52
  * brainEndpoint: process.env.GLASSBOX_BRAIN_ENDPOINT!,
18
- * brainJwt: process.env.GLASSBOX_BRAIN_JWT!,
19
- * appId: 'playbacksam',
53
+ * brainJwt: process.env.GLASSBOX_BRAIN_JWT!, // scoped JWT (RLS via app_id claim)
54
+ * brainAnonKey: process.env.GLASSBOX_BRAIN_ANON_KEY!, // project anon/publishable key (Supabase apikey header)
55
+ * appId: 'playbacksam',
20
56
  * });
21
57
  * export { proxy as GET };
22
58
  *
@@ -41,8 +77,21 @@ interface GlassboxRoutesConfig {
41
77
  extensionId: string;
42
78
  /** Brain endpoint base (e.g. https://kgauto-brain.supabase.co). Used by `proxy` for replay queries. */
43
79
  brainEndpoint: string;
44
- /** Scoped JWT for brain reads. Use the per-consumer JWT minted via migration 013 (claim: app_id). */
80
+ /** Scoped JWT for brain reads. Use the per-consumer JWT minted via migration 013 (claim: app_id). Drives RLS via the `app_id` claim; sent as `Authorization: Bearer <jwt>` only. */
45
81
  brainJwt: string;
82
+ /**
83
+ * Anon/publishable key for the Supabase `apikey` header. Supabase requires
84
+ * `apikey` to be one of the project's known keys (anon or service_role) —
85
+ * the scoped JWT in `brainJwt` doesn't qualify there. Pass the project's
86
+ * legacy `anon` key (JWT format, role=anon) or the modern `sb_publishable_...`
87
+ * key. Safe to expose at the wire (that's what "publishable" means).
88
+ *
89
+ * Pre-alpha.24 this was missing and `brainJwt` was used as apikey too — first
90
+ * real call always 401'd against real Supabase. Catching this required a
91
+ * pre-publish smoke against the real brain; unit tests with mocked fetch
92
+ * couldn't surface it. See L-117 in command-center/learnings.md.
93
+ */
94
+ brainAnonKey: string;
46
95
  /** App scope filter — must match the JWT's app_id claim. */
47
96
  appId: string;
48
97
  /**
@@ -70,4 +119,4 @@ interface GlassboxRoutes {
70
119
  }
71
120
  declare function createGlassboxRoutes(config: GlassboxRoutesConfig): GlassboxRoutes;
72
121
 
73
- export { type GlassboxRoutes, type GlassboxRoutesConfig, createGlassboxRoutes };
122
+ export { type GlassboxRoutes, type GlassboxRoutesConfig, type TraceDetail, type TraceSummary, createGlassboxRoutes };
@@ -2,6 +2,41 @@ import { G as GlassboxEvent } from '../types-DiWBWvxg.js';
2
2
  import '../ir-B_XX2LAO.js';
3
3
  import '../dialect.js';
4
4
 
5
+ /**
6
+ * Internal config + hook types for createGlassboxRoutes().
7
+ *
8
+ * The public contract lives on `GlassboxRoutesConfig` in ./index.ts; these
9
+ * are the narrower per-handler shapes consumed by proxy.ts and stream.ts.
10
+ */
11
+
12
+ /**
13
+ * Wire contract for the Glass-Box Chrome extension's brain-poll endpoint.
14
+ *
15
+ * The list mode of `proxy(req)` returns `{ traces: TraceSummary[] }`. The
16
+ * detail mode (`?traceId=<id>`) returns a single `TraceDetail`. These are
17
+ * the camelCase shapes the extension renderer expects — distinct from the
18
+ * snake_case `compile_outcomes` row shape that PostgREST returns. The
19
+ * factory's typed `rowToSummary` / `rowToDetail` transformer is the single
20
+ * canonical boundary between the DB shape and the wire shape (see
21
+ * `feedback_typed_boundary_transformers.md` in kgauto memory for the rule).
22
+ */
23
+ interface TraceSummary {
24
+ traceId: string;
25
+ appId: string;
26
+ archetype: string;
27
+ target: string;
28
+ createdAt: string;
29
+ tokensIn: number;
30
+ tokensOut: number;
31
+ estimatedCostUsd: number;
32
+ }
33
+ interface TraceDetail extends TraceSummary {
34
+ mutationsApplied: unknown[];
35
+ advisories: unknown[];
36
+ rawRequest?: unknown;
37
+ rawResponse?: unknown;
38
+ }
39
+
5
40
  /**
6
41
  * Public entry point for `@warmdrift/kgauto-compiler/glassbox-routes`.
7
42
  *
@@ -12,11 +47,12 @@ import '../dialect.js';
12
47
  * // app/api/glassbox/proxy/route.ts
13
48
  * import { createGlassboxRoutes } from '@warmdrift/kgauto-compiler/glassbox-routes';
14
49
  * const { proxy } = createGlassboxRoutes({
15
- * installToken: process.env.GLASSBOX_INSTALL_TOKEN!,
16
- * extensionId: process.env.GLASSBOX_EXTENSION_ID!,
50
+ * installToken: process.env.GLASSBOX_INSTALL_TOKEN!,
51
+ * extensionId: process.env.GLASSBOX_EXTENSION_ID!,
17
52
  * brainEndpoint: process.env.GLASSBOX_BRAIN_ENDPOINT!,
18
- * brainJwt: process.env.GLASSBOX_BRAIN_JWT!,
19
- * appId: 'playbacksam',
53
+ * brainJwt: process.env.GLASSBOX_BRAIN_JWT!, // scoped JWT (RLS via app_id claim)
54
+ * brainAnonKey: process.env.GLASSBOX_BRAIN_ANON_KEY!, // project anon/publishable key (Supabase apikey header)
55
+ * appId: 'playbacksam',
20
56
  * });
21
57
  * export { proxy as GET };
22
58
  *
@@ -41,8 +77,21 @@ interface GlassboxRoutesConfig {
41
77
  extensionId: string;
42
78
  /** Brain endpoint base (e.g. https://kgauto-brain.supabase.co). Used by `proxy` for replay queries. */
43
79
  brainEndpoint: string;
44
- /** Scoped JWT for brain reads. Use the per-consumer JWT minted via migration 013 (claim: app_id). */
80
+ /** Scoped JWT for brain reads. Use the per-consumer JWT minted via migration 013 (claim: app_id). Drives RLS via the `app_id` claim; sent as `Authorization: Bearer <jwt>` only. */
45
81
  brainJwt: string;
82
+ /**
83
+ * Anon/publishable key for the Supabase `apikey` header. Supabase requires
84
+ * `apikey` to be one of the project's known keys (anon or service_role) —
85
+ * the scoped JWT in `brainJwt` doesn't qualify there. Pass the project's
86
+ * legacy `anon` key (JWT format, role=anon) or the modern `sb_publishable_...`
87
+ * key. Safe to expose at the wire (that's what "publishable" means).
88
+ *
89
+ * Pre-alpha.24 this was missing and `brainJwt` was used as apikey too — first
90
+ * real call always 401'd against real Supabase. Catching this required a
91
+ * pre-publish smoke against the real brain; unit tests with mocked fetch
92
+ * couldn't surface it. See L-117 in command-center/learnings.md.
93
+ */
94
+ brainAnonKey: string;
46
95
  /** App scope filter — must match the JWT's app_id claim. */
47
96
  appId: string;
48
97
  /**
@@ -70,4 +119,4 @@ interface GlassboxRoutes {
70
119
  }
71
120
  declare function createGlassboxRoutes(config: GlassboxRoutesConfig): GlassboxRoutes;
72
121
 
73
- export { type GlassboxRoutes, type GlassboxRoutesConfig, createGlassboxRoutes };
122
+ export { type GlassboxRoutes, type GlassboxRoutesConfig, type TraceDetail, type TraceSummary, createGlassboxRoutes };
@@ -48,8 +48,11 @@ function checkAuth(req, config) {
48
48
  return jsonError(401, "unauthorized");
49
49
  }
50
50
  const origin = req.headers.get("Origin") ?? "";
51
- const expected = `chrome-extension://${config.extensionId}`;
52
- if (origin !== expected) {
51
+ const xExtId = req.headers.get("X-Glassbox-Extension-Id") ?? "";
52
+ const expectedOrigin = `chrome-extension://${config.extensionId}`;
53
+ const originOk = origin === expectedOrigin;
54
+ const xExtOk = xExtId.length > 0 && tokensEqual(xExtId, config.extensionId);
55
+ if (!originOk && !xExtOk) {
53
56
  return jsonError(403, "forbidden_origin");
54
57
  }
55
58
  return null;
@@ -82,12 +85,35 @@ function parseLimit(raw) {
82
85
  if (!Number.isFinite(n) || n <= 0) return DEFAULT_LIMIT;
83
86
  return Math.min(n, MAX_LIMIT);
84
87
  }
88
+ function rowToSummary(row) {
89
+ return {
90
+ traceId: typeof row.handle === "string" ? row.handle : "",
91
+ appId: typeof row.app_id === "string" ? row.app_id : "",
92
+ archetype: typeof row.intent_archetype === "string" ? row.intent_archetype : "",
93
+ target: typeof row.model === "string" ? row.model : "",
94
+ createdAt: typeof row.created_at === "string" ? row.created_at : "",
95
+ tokensIn: typeof row.tokens_in === "number" ? row.tokens_in : 0,
96
+ tokensOut: typeof row.tokens_out === "number" ? row.tokens_out : 0,
97
+ estimatedCostUsd: typeof row.cost_usd_actual === "number" ? row.cost_usd_actual : 0
98
+ };
99
+ }
100
+ function rowToDetail(row) {
101
+ return {
102
+ ...rowToSummary(row),
103
+ mutationsApplied: Array.isArray(row.mutations_applied) ? row.mutations_applied : [],
104
+ advisories: [],
105
+ // compile_outcomes has no advisories column today
106
+ rawRequest: row.prompt_preview ?? void 0,
107
+ rawResponse: row.response_preview ?? void 0
108
+ };
109
+ }
85
110
  function createProxyHandler(config) {
86
111
  const {
87
112
  installToken,
88
113
  extensionId,
89
114
  brainEndpoint,
90
115
  brainJwt,
116
+ brainAnonKey,
91
117
  appId,
92
118
  scrub,
93
119
  fetch: fetchImpl
@@ -103,7 +129,7 @@ function createProxyHandler(config) {
103
129
  const qs = new URLSearchParams();
104
130
  qs.set("app_id", `eq.${appId}`);
105
131
  if (traceId) {
106
- qs.set("trace_id", `eq.${traceId}`);
132
+ qs.set("handle", `eq.${traceId}`);
107
133
  } else {
108
134
  qs.set("order", "created_at.desc");
109
135
  qs.set("limit", String(limit));
@@ -114,8 +140,13 @@ function createProxyHandler(config) {
114
140
  brainRes = await doFetch(brainUrl, {
115
141
  method: "GET",
116
142
  headers: {
143
+ // Authorization carries the scoped JWT — drives RLS via app_id claim.
117
144
  Authorization: `Bearer ${brainJwt}`,
118
- apikey: brainJwt,
145
+ // apikey MUST be one of the project's known keys (anon or
146
+ // service_role). Supabase rejects any other JWT here, even when
147
+ // HS256-signed with the same secret. Pre-alpha.24 this was set to
148
+ // brainJwt and silently 401'd against real Supabase. See L-117.
149
+ apikey: brainAnonKey,
119
150
  Accept: "application/json"
120
151
  }
121
152
  });
@@ -140,8 +171,15 @@ function createProxyHandler(config) {
140
171
  if (!Array.isArray(rows)) {
141
172
  return jsonError2(502, "brain_unavailable");
142
173
  }
143
- const scrubbed = rows.map((row) => applyScrub(row, scrub));
144
- return jsonResponse(200, scrubbed);
174
+ const scrubbed = rows.map(
175
+ (row) => applyScrub(row, scrub)
176
+ );
177
+ if (traceId) {
178
+ const first = scrubbed[0];
179
+ if (!first) return jsonError2(404, "not_found");
180
+ return jsonResponse(200, rowToDetail(first));
181
+ }
182
+ return jsonResponse(200, { traces: scrubbed.map(rowToSummary) });
145
183
  };
146
184
  }
147
185
 
@@ -533,12 +571,14 @@ function createGlassboxRoutes(config) {
533
571
  const extensionId = requireString("extensionId", config.extensionId);
534
572
  const brainEndpoint = requireString("brainEndpoint", config.brainEndpoint);
535
573
  const brainJwt = requireString("brainJwt", config.brainJwt);
574
+ const brainAnonKey = requireString("brainAnonKey", config.brainAnonKey);
536
575
  const appId = requireString("appId", config.appId);
537
576
  const proxy = createProxyHandler({
538
577
  installToken,
539
578
  extensionId,
540
579
  brainEndpoint,
541
580
  brainJwt,
581
+ brainAnonKey,
542
582
  appId,
543
583
  scrub: config.scrub,
544
584
  fetch: config.fetch
@@ -27,8 +27,11 @@ function checkAuth(req, config) {
27
27
  return jsonError(401, "unauthorized");
28
28
  }
29
29
  const origin = req.headers.get("Origin") ?? "";
30
- const expected = `chrome-extension://${config.extensionId}`;
31
- if (origin !== expected) {
30
+ const xExtId = req.headers.get("X-Glassbox-Extension-Id") ?? "";
31
+ const expectedOrigin = `chrome-extension://${config.extensionId}`;
32
+ const originOk = origin === expectedOrigin;
33
+ const xExtOk = xExtId.length > 0 && tokensEqual(xExtId, config.extensionId);
34
+ if (!originOk && !xExtOk) {
32
35
  return jsonError(403, "forbidden_origin");
33
36
  }
34
37
  return null;
@@ -61,12 +64,35 @@ function parseLimit(raw) {
61
64
  if (!Number.isFinite(n) || n <= 0) return DEFAULT_LIMIT;
62
65
  return Math.min(n, MAX_LIMIT);
63
66
  }
67
+ function rowToSummary(row) {
68
+ return {
69
+ traceId: typeof row.handle === "string" ? row.handle : "",
70
+ appId: typeof row.app_id === "string" ? row.app_id : "",
71
+ archetype: typeof row.intent_archetype === "string" ? row.intent_archetype : "",
72
+ target: typeof row.model === "string" ? row.model : "",
73
+ createdAt: typeof row.created_at === "string" ? row.created_at : "",
74
+ tokensIn: typeof row.tokens_in === "number" ? row.tokens_in : 0,
75
+ tokensOut: typeof row.tokens_out === "number" ? row.tokens_out : 0,
76
+ estimatedCostUsd: typeof row.cost_usd_actual === "number" ? row.cost_usd_actual : 0
77
+ };
78
+ }
79
+ function rowToDetail(row) {
80
+ return {
81
+ ...rowToSummary(row),
82
+ mutationsApplied: Array.isArray(row.mutations_applied) ? row.mutations_applied : [],
83
+ advisories: [],
84
+ // compile_outcomes has no advisories column today
85
+ rawRequest: row.prompt_preview ?? void 0,
86
+ rawResponse: row.response_preview ?? void 0
87
+ };
88
+ }
64
89
  function createProxyHandler(config) {
65
90
  const {
66
91
  installToken,
67
92
  extensionId,
68
93
  brainEndpoint,
69
94
  brainJwt,
95
+ brainAnonKey,
70
96
  appId,
71
97
  scrub,
72
98
  fetch: fetchImpl
@@ -82,7 +108,7 @@ function createProxyHandler(config) {
82
108
  const qs = new URLSearchParams();
83
109
  qs.set("app_id", `eq.${appId}`);
84
110
  if (traceId) {
85
- qs.set("trace_id", `eq.${traceId}`);
111
+ qs.set("handle", `eq.${traceId}`);
86
112
  } else {
87
113
  qs.set("order", "created_at.desc");
88
114
  qs.set("limit", String(limit));
@@ -93,8 +119,13 @@ function createProxyHandler(config) {
93
119
  brainRes = await doFetch(brainUrl, {
94
120
  method: "GET",
95
121
  headers: {
122
+ // Authorization carries the scoped JWT — drives RLS via app_id claim.
96
123
  Authorization: `Bearer ${brainJwt}`,
97
- apikey: brainJwt,
124
+ // apikey MUST be one of the project's known keys (anon or
125
+ // service_role). Supabase rejects any other JWT here, even when
126
+ // HS256-signed with the same secret. Pre-alpha.24 this was set to
127
+ // brainJwt and silently 401'd against real Supabase. See L-117.
128
+ apikey: brainAnonKey,
98
129
  Accept: "application/json"
99
130
  }
100
131
  });
@@ -119,8 +150,15 @@ function createProxyHandler(config) {
119
150
  if (!Array.isArray(rows)) {
120
151
  return jsonError2(502, "brain_unavailable");
121
152
  }
122
- const scrubbed = rows.map((row) => applyScrub(row, scrub));
123
- return jsonResponse(200, scrubbed);
153
+ const scrubbed = rows.map(
154
+ (row) => applyScrub(row, scrub)
155
+ );
156
+ if (traceId) {
157
+ const first = scrubbed[0];
158
+ if (!first) return jsonError2(404, "not_found");
159
+ return jsonResponse(200, rowToDetail(first));
160
+ }
161
+ return jsonResponse(200, { traces: scrubbed.map(rowToSummary) });
124
162
  };
125
163
  }
126
164
 
@@ -243,12 +281,14 @@ function createGlassboxRoutes(config) {
243
281
  const extensionId = requireString("extensionId", config.extensionId);
244
282
  const brainEndpoint = requireString("brainEndpoint", config.brainEndpoint);
245
283
  const brainJwt = requireString("brainJwt", config.brainJwt);
284
+ const brainAnonKey = requireString("brainAnonKey", config.brainAnonKey);
246
285
  const appId = requireString("appId", config.appId);
247
286
  const proxy = createProxyHandler({
248
287
  installToken,
249
288
  extensionId,
250
289
  brainEndpoint,
251
290
  brainJwt,
291
+ brainAnonKey,
252
292
  appId,
253
293
  scrub: config.scrub,
254
294
  fetch: config.fetch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warmdrift/kgauto-compiler",
3
- "version": "2.0.0-alpha.23",
3
+ "version": "2.0.0-alpha.25",
4
4
  "description": "Prompt compiler + central learning brain for multi-model AI apps. Swap models without rewriting prompts.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",