@introspection-sdk/introspection-browser 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/README.md +45 -2
- package/dist/api/client.d.ts +31 -3
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +34 -3
- package/dist/api/client.js.map +1 -1
- package/dist/api/http.d.ts +28 -5
- package/dist/api/http.d.ts.map +1 -1
- package/dist/api/http.js +25 -6
- package/dist/api/http.js.map +1 -1
- package/dist/api/index.d.ts +3 -3
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +2 -2
- package/dist/api/index.js.map +1 -1
- package/package.json +3 -3
- package/src/api/client.ts +54 -3
- package/src/api/http.ts +36 -6
- package/src/api/index.ts +7 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
|
|
2
|
-
> @introspection-sdk/introspection-browser@0.
|
|
2
|
+
> @introspection-sdk/introspection-browser@0.16.0 build /home/runner/work/introspection-js-sdk/introspection-js-sdk/packages/introspection-browser
|
|
3
3
|
> tsc
|
|
4
4
|
|
package/README.md
CHANGED
|
@@ -50,7 +50,8 @@ customer web origins.
|
|
|
50
50
|
`client.runtimes.resolve("support-agent")`) and supplies the Data
|
|
51
51
|
Plane URL, so the SPA needs no Introspection config of its own.
|
|
52
52
|
2. `client.connect()` redeems the token at the **Data Plane**
|
|
53
|
-
`POST /v1/oauth/exchange` for
|
|
53
|
+
`POST /v1/oauth/exchange` for an HttpOnly session cookie, named for the
|
|
54
|
+
token's environment lane (see [Environments](#environments)).
|
|
54
55
|
3. `client.tasks.start({ runtime_id })` and friends ride that cookie against
|
|
55
56
|
the Data Plane for tasks, files, conversations, and shares.
|
|
56
57
|
|
|
@@ -69,7 +70,7 @@ const client = new IntrospectionApiClient({
|
|
|
69
70
|
getToken: () => token,
|
|
70
71
|
});
|
|
71
72
|
|
|
72
|
-
await client.connect(); // ->
|
|
73
|
+
await client.connect(); // -> intro_dp_<environment> session cookie
|
|
73
74
|
|
|
74
75
|
const run = await client.tasks.start({
|
|
75
76
|
prompt: "Summarize my latest order",
|
|
@@ -94,6 +95,48 @@ as soon as it's provisioned; omit to use the deployment default. Clamped to the
|
|
|
94
95
|
task timeout. When no `runtime_id` is supplied, pass `agent_name` to select a
|
|
95
96
|
named recipe agent instead.
|
|
96
97
|
|
|
98
|
+
### Environments
|
|
99
|
+
|
|
100
|
+
The session cookie is named for its environment lane —
|
|
101
|
+
`intro_dp_development`, `intro_dp_staging`, `intro_dp_production` — so an app
|
|
102
|
+
running more than one lane holds a live session for **each** in one browser. A
|
|
103
|
+
single shared name gave the browser one slot, so a second `connect()` evicted
|
|
104
|
+
the first.
|
|
105
|
+
|
|
106
|
+
**You never pass the environment in.** Your backend picks the lane when it
|
|
107
|
+
mints the token; the Data Plane reads it off the token's claim and returns it,
|
|
108
|
+
and the client sends it back as `x-introspection-environment` so the right
|
|
109
|
+
cookie is resolved:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
const dev = new IntrospectionApiClient({
|
|
113
|
+
dpUrl,
|
|
114
|
+
getToken: () => mintToken({ environment: "development" }),
|
|
115
|
+
});
|
|
116
|
+
const prod = new IntrospectionApiClient({
|
|
117
|
+
dpUrl,
|
|
118
|
+
getToken: () => mintToken({ environment: "production" }),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await Promise.all([dev.connect(), prod.connect()]);
|
|
122
|
+
|
|
123
|
+
dev.environment; // "development" -> cookie intro_dp_development
|
|
124
|
+
prod.environment; // "production" -> cookie intro_dp_production
|
|
125
|
+
|
|
126
|
+
await dev.tasks.list(); // x-introspection-environment: development
|
|
127
|
+
await prod.tasks.list(); // x-introspection-environment: production
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`client.environment` is `undefined` until `connect()` resolves. The optional
|
|
131
|
+
`environment` constructor option sends the header before that first exchange;
|
|
132
|
+
if it disagrees with the token, `connect()` throws rather than silently
|
|
133
|
+
preferring one — the token is the authority.
|
|
134
|
+
|
|
135
|
+
> **Requires a Data Plane that returns `environment` from
|
|
136
|
+
> `/v1/oauth/exchange`.** Against an older deployment `client.environment`
|
|
137
|
+
> stays `undefined`, no header is sent, and a single-lane app keeps working
|
|
138
|
+
> unchanged.
|
|
139
|
+
|
|
97
140
|
## Files and conversations
|
|
98
141
|
|
|
99
142
|
The same cookie session also reaches `/v1/files` and (read-only)
|
package/dist/api/client.d.ts
CHANGED
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
* is needed, resolves its `runtime_id` server-side. The SPA fetches
|
|
15
15
|
* the token through the `getToken` callback.
|
|
16
16
|
* 2. `connect()` redeems the token at the DP `POST /v1/oauth/exchange`
|
|
17
|
-
* for
|
|
17
|
+
* for an HttpOnly session cookie, named for the token's environment lane
|
|
18
|
+
* (`intro_dp_development` / `intro_dp_staging` / `intro_dp_production`).
|
|
19
|
+
* Distinct names mean an app running several lanes holds a live session
|
|
20
|
+
* for each in one browser instead of the newest evicting the last.
|
|
18
21
|
* 3. Every subsequent call rides that cookie (`credentials: "include"`)
|
|
19
22
|
* — `client.tasks.start({ runtime_id })`, `.get(...)`, run streaming,
|
|
20
23
|
* etc.
|
|
@@ -24,6 +27,7 @@
|
|
|
24
27
|
* before retrying.
|
|
25
28
|
*/
|
|
26
29
|
import { ConversationsClient, FilesClient, SharesClient } from "@introspection-sdk/http";
|
|
30
|
+
import type { Environment } from "@introspection-sdk/types";
|
|
27
31
|
import { TasksClient } from "./tasks.js";
|
|
28
32
|
export interface IntrospectionApiClientOptions {
|
|
29
33
|
/**
|
|
@@ -44,11 +48,29 @@ export interface IntrospectionApiClientOptions {
|
|
|
44
48
|
fetch?: typeof fetch;
|
|
45
49
|
/** Extra headers merged into every DP request. */
|
|
46
50
|
additionalHeaders?: Record<string, string>;
|
|
51
|
+
/**
|
|
52
|
+
* Environment lane this client talks to.
|
|
53
|
+
*
|
|
54
|
+
* Normally omit it. `connect()` learns the lane from the exchange response,
|
|
55
|
+
* which the DP derives from the token's own `environment` claim — that is the
|
|
56
|
+
* authoritative source, and it is why a client cannot simply declare a lane
|
|
57
|
+
* it has no token for.
|
|
58
|
+
*
|
|
59
|
+
* Set it only to send the lane header before the first `connect()` resolves.
|
|
60
|
+
* If it disagrees with the token, `connect()` throws rather than silently
|
|
61
|
+
* preferring one.
|
|
62
|
+
*/
|
|
63
|
+
environment?: Environment;
|
|
47
64
|
}
|
|
48
65
|
export declare class IntrospectionApiClient {
|
|
49
66
|
private readonly opts;
|
|
50
67
|
private readonly fetchImpl;
|
|
51
68
|
private readonly cookieClients;
|
|
69
|
+
/**
|
|
70
|
+
* Lane established by the most recent exchange. Per instance, never module
|
|
71
|
+
* scope — two clients on one page holding different lanes is the point.
|
|
72
|
+
*/
|
|
73
|
+
private resolvedEnvironment?;
|
|
52
74
|
constructor(opts: IntrospectionApiClientOptions);
|
|
53
75
|
/** `/v1/tasks` operations bound to the DP session cookie. */
|
|
54
76
|
get tasks(): TasksClient;
|
|
@@ -59,8 +81,14 @@ export declare class IntrospectionApiClient {
|
|
|
59
81
|
/** `/v1/shares` read-sharing grants bound to the session cookie. */
|
|
60
82
|
get shares(): SharesClient;
|
|
61
83
|
/**
|
|
62
|
-
*
|
|
63
|
-
* `
|
|
84
|
+
* Environment lane this client is connected to, once `connect()` has
|
|
85
|
+
* resolved. `undefined` before the first exchange, unless the constructor
|
|
86
|
+
* was given one.
|
|
87
|
+
*/
|
|
88
|
+
get environment(): Environment | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Mint a token via `getToken` and redeem it at the DP for the session
|
|
91
|
+
* cookie. Call once before issuing task requests.
|
|
64
92
|
*/
|
|
65
93
|
connect(): Promise<void>;
|
|
66
94
|
private exchange;
|
package/dist/api/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,YAAY,EACb,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AASzC,MAAM,WAAW,6BAA6B;IAC5C;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,QAAQ,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzC,2DAA2D;IAC3D,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,kDAAkD;IAClD,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,sBAAsB;IASrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IARjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C;;;OAGG;IACH,OAAO,CAAC,mBAAmB,CAAC,CAAc;gBAEb,IAAI,EAAE,6BAA6B;IAyBhE,6DAA6D;IAC7D,IAAI,KAAK,IAAI,WAAW,CAEvB;IAED,6DAA6D;IAC7D,IAAI,KAAK,IAAI,WAAW,CAEvB;IAED,4EAA4E;IAC5E,IAAI,aAAa,IAAI,mBAAmB,CAEvC;IAED,oEAAoE;IACpE,IAAI,MAAM,IAAI,YAAY,CAEzB;IAED;;;;OAIG;IACH,IAAI,WAAW,IAAI,WAAW,GAAG,SAAS,CAEzC;IAED;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAIhB,QAAQ;IAmCtB,0EAA0E;YAC5D,UAAU;CAQzB"}
|
package/dist/api/client.js
CHANGED
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
* is needed, resolves its `runtime_id` server-side. The SPA fetches
|
|
15
15
|
* the token through the `getToken` callback.
|
|
16
16
|
* 2. `connect()` redeems the token at the DP `POST /v1/oauth/exchange`
|
|
17
|
-
* for
|
|
17
|
+
* for an HttpOnly session cookie, named for the token's environment lane
|
|
18
|
+
* (`intro_dp_development` / `intro_dp_staging` / `intro_dp_production`).
|
|
19
|
+
* Distinct names mean an app running several lanes holds a live session
|
|
20
|
+
* for each in one browser instead of the newest evicting the last.
|
|
18
21
|
* 3. Every subsequent call rides that cookie (`credentials: "include"`)
|
|
19
22
|
* — `client.tasks.start({ runtime_id })`, `.get(...)`, run streaming,
|
|
20
23
|
* etc.
|
|
@@ -31,6 +34,11 @@ export class IntrospectionApiClient {
|
|
|
31
34
|
opts;
|
|
32
35
|
fetchImpl;
|
|
33
36
|
cookieClients;
|
|
37
|
+
/**
|
|
38
|
+
* Lane established by the most recent exchange. Per instance, never module
|
|
39
|
+
* scope — two clients on one page holding different lanes is the point.
|
|
40
|
+
*/
|
|
41
|
+
resolvedEnvironment;
|
|
34
42
|
constructor(opts) {
|
|
35
43
|
this.opts = opts;
|
|
36
44
|
// Native browser `fetch` throws "Illegal invocation" when called as a
|
|
@@ -41,11 +49,13 @@ export class IntrospectionApiClient {
|
|
|
41
49
|
if (!opts.dpUrl) {
|
|
42
50
|
throw new Error("IntrospectionApiClient requires a dpUrl");
|
|
43
51
|
}
|
|
52
|
+
this.resolvedEnvironment = opts.environment;
|
|
44
53
|
const http = new BrowserHttpClient({
|
|
45
54
|
apiUrl: opts.dpUrl,
|
|
46
55
|
additionalHeaders: opts.additionalHeaders,
|
|
47
56
|
fetch: this.fetchImpl,
|
|
48
57
|
onUnauthorized: () => this.reexchange(),
|
|
58
|
+
environment: () => this.resolvedEnvironment,
|
|
49
59
|
});
|
|
50
60
|
this.cookieClients = {
|
|
51
61
|
tasks: new TasksClient(http),
|
|
@@ -71,8 +81,16 @@ export class IntrospectionApiClient {
|
|
|
71
81
|
return this.cookieClients.shares;
|
|
72
82
|
}
|
|
73
83
|
/**
|
|
74
|
-
*
|
|
75
|
-
* `
|
|
84
|
+
* Environment lane this client is connected to, once `connect()` has
|
|
85
|
+
* resolved. `undefined` before the first exchange, unless the constructor
|
|
86
|
+
* was given one.
|
|
87
|
+
*/
|
|
88
|
+
get environment() {
|
|
89
|
+
return this.resolvedEnvironment;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Mint a token via `getToken` and redeem it at the DP for the session
|
|
93
|
+
* cookie. Call once before issuing task requests.
|
|
76
94
|
*/
|
|
77
95
|
async connect() {
|
|
78
96
|
await this.exchange();
|
|
@@ -90,6 +108,19 @@ export class IntrospectionApiClient {
|
|
|
90
108
|
});
|
|
91
109
|
if (!res.ok)
|
|
92
110
|
throw await toApiError(res);
|
|
111
|
+
// The DP echoes the lane it resolved from the token's claim, so the client
|
|
112
|
+
// learns which cookie it just established without being told. Re-read on
|
|
113
|
+
// every exchange, including the 401 recovery path: if the broker has since
|
|
114
|
+
// switched lanes, a stale value would name a cookie we no longer hold.
|
|
115
|
+
const body = (await res.json().catch(() => ({})));
|
|
116
|
+
if (body.environment) {
|
|
117
|
+
if (this.opts.environment && this.opts.environment !== body.environment) {
|
|
118
|
+
throw new Error(`IntrospectionApiClient is configured for environment ` +
|
|
119
|
+
`"${this.opts.environment}" but getToken() returned a token for ` +
|
|
120
|
+
`"${body.environment}".`);
|
|
121
|
+
}
|
|
122
|
+
this.resolvedEnvironment = body.environment;
|
|
123
|
+
}
|
|
93
124
|
}
|
|
94
125
|
/** 401 recovery: re-exchange, reporting success so the call can retry. */
|
|
95
126
|
async reexchange() {
|
package/dist/api/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,YAAY,GACb,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA2CzC,MAAM,OAAO,sBAAsB;IASJ;IARZ,SAAS,CAAe;IACxB,aAAa,CAAgB;IAC9C;;;OAGG;IACK,mBAAmB,CAAe;IAE1C,YAA6B,IAAmC;QAAnC,SAAI,GAAJ,IAAI,CAA+B;QAC9D,sEAAsE;QACtE,qEAAqE;QACrE,mEAAmE;QACnE,6EAA6E;QAC7E,IAAI,CAAC,SAAS,GAAG,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjD,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;QAC7D,CAAC;QACD,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,WAAW,CAAC;QAC5C,MAAM,IAAI,GAAG,IAAI,iBAAiB,CAAC;YACjC,MAAM,EAAE,IAAI,CAAC,KAAK;YAClB,iBAAiB,EAAE,IAAI,CAAC,iBAAiB;YACzC,KAAK,EAAE,IAAI,CAAC,SAAS;YACrB,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE;YACvC,WAAW,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,mBAAmB;SAC5C,CAAC,CAAC;QACH,IAAI,CAAC,aAAa,GAAG;YACnB,KAAK,EAAE,IAAI,WAAW,CAAC,IAAI,CAAC;YAC5B,KAAK,EAAE,IAAI,WAAW,CAAC,IAAI,CAAC;YAC5B,aAAa,EAAE,IAAI,mBAAmB,CAAC,IAAI,CAAC;YAC5C,MAAM,EAAE,IAAI,YAAY,CAAC,IAAI,CAAC;SAC/B,CAAC;IACJ,CAAC;IAED,6DAA6D;IAC7D,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC;IAClC,CAAC;IAED,6DAA6D;IAC7D,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC;IAClC,CAAC;IAED,4EAA4E;IAC5E,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC;IAC1C,CAAC;IAED,oEAAoE;IACpE,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC;IACnC,CAAC;IAED;;;;OAIG;IACH,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,mBAAmB,CAAC;IAClC,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,OAAO;QACX,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;IACxB,CAAC;IAEO,KAAK,CAAC,QAAQ;QACpB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACzC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,CAC9B,GAAG,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,oBAAoB,EAC1D;YACE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,IAAI,EAAE,CAAC;aACvC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC;YAC/B,WAAW,EAAE,SAAS;SACvB,CACF,CAAC;QACF,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC;QAEzC,2EAA2E;QAC3E,yEAAyE;QACzE,2EAA2E;QAC3E,uEAAuE;QACvE,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAE/C,CAAC;QACF,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC;gBACxE,MAAM,IAAI,KAAK,CACb,uDAAuD;oBACrD,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,wCAAwC;oBACjE,IAAI,IAAI,CAAC,WAAW,IAAI,CAC3B,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,WAAW,CAAC;QAC9C,CAAC;IACH,CAAC;IAED,0EAA0E;IAClE,KAAK,CAAC,UAAU;QACtB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;YACtB,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF"}
|
package/dist/api/http.d.ts
CHANGED
|
@@ -4,14 +4,29 @@
|
|
|
4
4
|
* The request/response machinery — URL joining, query building, body
|
|
5
5
|
* serialization, error mapping, AG-UI stream parsing — lives in the shared
|
|
6
6
|
* `@introspection-sdk/http` {@link BaseHttpClient}. This module pins the
|
|
7
|
-
* browser auth strategy on top of it: the DP is reached with
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* browser auth strategy on top of it: the DP is reached with an HttpOnly
|
|
8
|
+
* session cookie minted by `POST /v1/oauth/exchange`, so every request opts
|
|
9
|
+
* into `credentials: "include"` and sends NO bearer token — no Introspection
|
|
10
|
+
* credential ever lives in JavaScript. A 401 (expired session cookie)
|
|
11
|
+
* triggers a single `onUnauthorized` refresh + retry.
|
|
12
|
+
*
|
|
13
|
+
* The session cookie is named for its environment lane
|
|
14
|
+
* (`intro_dp_development` / `intro_dp_staging` / `intro_dp_production`), so an
|
|
15
|
+
* app running several lanes holds a live session for each in one browser. When
|
|
16
|
+
* more than one is present the DP cannot tell which a request means, so we name
|
|
17
|
+
* it in {@link ENVIRONMENT_HEADER}.
|
|
12
18
|
*/
|
|
13
19
|
import { BaseHttpClient, stripTrailingSlash, toApiError } from "@introspection-sdk/http";
|
|
20
|
+
import type { Environment } from "@introspection-sdk/types";
|
|
14
21
|
export { stripTrailingSlash, toApiError };
|
|
22
|
+
/**
|
|
23
|
+
* Names which environment lane's session cookie the DP should resolve.
|
|
24
|
+
*
|
|
25
|
+
* A selector, not a credential: the cookie it names is still validated
|
|
26
|
+
* server-side, so naming a lane this browser holds no cookie for resolves to
|
|
27
|
+
* nothing rather than granting anything.
|
|
28
|
+
*/
|
|
29
|
+
export declare const ENVIRONMENT_HEADER = "x-introspection-environment";
|
|
15
30
|
export interface BrowserHttpConfig {
|
|
16
31
|
/** DP REST base URL every request is prefixed with. */
|
|
17
32
|
apiUrl: string;
|
|
@@ -25,6 +40,14 @@ export interface BrowserHttpConfig {
|
|
|
25
40
|
* `false` to surface the original error.
|
|
26
41
|
*/
|
|
27
42
|
onUnauthorized?: () => Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* Reads the lane established by the most recent exchange.
|
|
45
|
+
*
|
|
46
|
+
* Called per request rather than captured at construction, because the lane
|
|
47
|
+
* is not known until `connect()` resolves — and it can change under a
|
|
48
|
+
* re-exchange.
|
|
49
|
+
*/
|
|
50
|
+
environment?: () => Environment | undefined;
|
|
28
51
|
}
|
|
29
52
|
/**
|
|
30
53
|
* Cookie-authenticated HTTP wrapper bound to one DP endpoint. Has no
|
package/dist/api/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/api/http.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/api/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,UAAU,EACX,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAK5D,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,CAAC;AAE1C;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,gCAAgC,CAAC;AAEhE,MAAM,WAAW,iBAAiB;IAChC,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,2DAA2D;IAC3D,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,WAAW,GAAG,SAAS,CAAC;CAC7C;AAED;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,cAAc;gBACvC,GAAG,EAAE,iBAAiB;CAkBnC"}
|
package/dist/api/http.js
CHANGED
|
@@ -4,16 +4,30 @@
|
|
|
4
4
|
* The request/response machinery — URL joining, query building, body
|
|
5
5
|
* serialization, error mapping, AG-UI stream parsing — lives in the shared
|
|
6
6
|
* `@introspection-sdk/http` {@link BaseHttpClient}. This module pins the
|
|
7
|
-
* browser auth strategy on top of it: the DP is reached with
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* browser auth strategy on top of it: the DP is reached with an HttpOnly
|
|
8
|
+
* session cookie minted by `POST /v1/oauth/exchange`, so every request opts
|
|
9
|
+
* into `credentials: "include"` and sends NO bearer token — no Introspection
|
|
10
|
+
* credential ever lives in JavaScript. A 401 (expired session cookie)
|
|
11
|
+
* triggers a single `onUnauthorized` refresh + retry.
|
|
12
|
+
*
|
|
13
|
+
* The session cookie is named for its environment lane
|
|
14
|
+
* (`intro_dp_development` / `intro_dp_staging` / `intro_dp_production`), so an
|
|
15
|
+
* app running several lanes holds a live session for each in one browser. When
|
|
16
|
+
* more than one is present the DP cannot tell which a request means, so we name
|
|
17
|
+
* it in {@link ENVIRONMENT_HEADER}.
|
|
12
18
|
*/
|
|
13
19
|
import { BaseHttpClient, stripTrailingSlash, toApiError, } from "@introspection-sdk/http";
|
|
14
20
|
import { resolveBrowserFetch } from "./fetch.js";
|
|
15
21
|
// Re-exported for `client.ts`, which redeems the DP session on `connect()`.
|
|
16
22
|
export { stripTrailingSlash, toApiError };
|
|
23
|
+
/**
|
|
24
|
+
* Names which environment lane's session cookie the DP should resolve.
|
|
25
|
+
*
|
|
26
|
+
* A selector, not a credential: the cookie it names is still validated
|
|
27
|
+
* server-side, so naming a lane this browser holds no cookie for resolves to
|
|
28
|
+
* nothing rather than granting anything.
|
|
29
|
+
*/
|
|
30
|
+
export const ENVIRONMENT_HEADER = "x-introspection-environment";
|
|
17
31
|
/**
|
|
18
32
|
* Cookie-authenticated HTTP wrapper bound to one DP endpoint. Has no
|
|
19
33
|
* opinion about which resource it serves — the caller picks the path.
|
|
@@ -26,7 +40,12 @@ export class BrowserHttpClient extends BaseHttpClient {
|
|
|
26
40
|
fetch: resolveBrowserFetch(cfg.fetch),
|
|
27
41
|
additionalHeaders: cfg.additionalHeaders,
|
|
28
42
|
transport: {
|
|
29
|
-
|
|
43
|
+
// Still no bearer token — the HttpOnly cookie is the credential. The
|
|
44
|
+
// only per-request contribution is which lane's cookie to resolve.
|
|
45
|
+
authHeaders: () => {
|
|
46
|
+
const environment = cfg.environment?.();
|
|
47
|
+
return environment ? { [ENVIRONMENT_HEADER]: environment } : {};
|
|
48
|
+
},
|
|
30
49
|
credentials: "include",
|
|
31
50
|
onUnauthorized: cfg.onUnauthorized,
|
|
32
51
|
},
|
package/dist/api/http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/api/http.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/api/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,UAAU,GACX,MAAM,yBAAyB,CAAC;AAIjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEjD,4EAA4E;AAC5E,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,6BAA6B,CAAC;AAyBhE;;;GAGG;AACH,MAAM,OAAO,iBAAkB,SAAQ,cAAc;IACnD,YAAY,GAAsB;QAChC,KAAK,CAAC;YACJ,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,uEAAuE;YACvE,KAAK,EAAE,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC;YACrC,iBAAiB,EAAE,GAAG,CAAC,iBAAiB;YACxC,SAAS,EAAE;gBACT,qEAAqE;gBACrE,mEAAmE;gBACnE,WAAW,EAAE,GAA2B,EAAE;oBACxC,MAAM,WAAW,GAAG,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC;oBACxC,OAAO,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,kBAAkB,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClE,CAAC;gBACD,WAAW,EAAE,SAAS;gBACtB,cAAc,EAAE,GAAG,CAAC,cAAc;aACnC;SACF,CAAC,CAAC;IACL,CAAC;CACF"}
|
package/dist/api/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* getToken: () => token,
|
|
27
27
|
* });
|
|
28
28
|
*
|
|
29
|
-
* await client.connect(); // ->
|
|
29
|
+
* await client.connect(); // -> intro_dp_<environment> session cookie
|
|
30
30
|
* const run = await client.tasks.start({
|
|
31
31
|
* prompt: "Summarize my latest order",
|
|
32
32
|
* runtime_id: runtimeId,
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
export { IntrospectionApiClient, type IntrospectionApiClientOptions, } from "./client.js";
|
|
38
38
|
export { TasksClient, TaskRunsClient, RunHandle, type CreateTaskParams, type StartTaskParams, } from "./tasks.js";
|
|
39
39
|
export { ConversationItemsClient, ConversationsClient, FileVersionsClient, FilesClient, SharesClient, type FileUploadBody, } from "@introspection-sdk/http";
|
|
40
|
-
export { BrowserHttpClient, type BrowserHttpConfig } from "./http.js";
|
|
40
|
+
export { BrowserHttpClient, ENVIRONMENT_HEADER, type BrowserHttpConfig, } from "./http.js";
|
|
41
41
|
export { Paginator, cursorPaginate, type PageSource, } from "@introspection-sdk/http";
|
|
42
42
|
export { EventType } from "@introspection-sdk/types";
|
|
43
|
-
export type { AGUIEvent, BaseEvent, Interrupt, Message, ResumeEntry, RunAgentInput, Task, TaskRun, TaskStatus, TaskRunKind, TaskCreateResponse, TaskListParams, TaskUpdateParams, TaskRunCreateParams, TaskRunResumeParams, TaskCancelResponse, TaskCancelOptions, RunIdentityInput, Paginated, File, FileType, FileListParams, FileUpdateParams, FileCreateTextParams, ListParams, ConversationSummary, ConversationSortField, ConversationItem, ConversationItemList, ConversationListParams, ConversationItemListParams, ConversationItemInclude, ConversationResponse, ResourceShare, ShareResourceType, ShareCreateParams, ShareListParams, } from "@introspection-sdk/types";
|
|
43
|
+
export type { AGUIEvent, BaseEvent, Environment, Interrupt, Message, ResumeEntry, RunAgentInput, Task, TaskRun, TaskStatus, TaskRunKind, TaskCreateResponse, TaskListParams, TaskUpdateParams, TaskRunCreateParams, TaskRunResumeParams, TaskCancelResponse, TaskCancelOptions, RunIdentityInput, Paginated, File, FileType, FileListParams, FileUpdateParams, FileCreateTextParams, ListParams, ConversationSummary, ConversationSortField, ConversationItem, ConversationItemList, ConversationListParams, ConversationItemListParams, ConversationItemInclude, ConversationResponse, ResourceShare, ShareResourceType, ShareCreateParams, ShareListParams, } from "@introspection-sdk/types";
|
|
44
44
|
export { IntrospectionAPIError, AuthenticationError, InsufficientScopeError, NotFoundError, NetworkError, RunnerExpiredError, } from "@introspection-sdk/types";
|
|
45
45
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/api/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EACL,sBAAsB,EACtB,KAAK,6BAA6B,GACnC,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,WAAW,EACX,cAAc,EACd,SAAS,EACT,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,EAClB,WAAW,EACX,YAAY,EACZ,KAAK,cAAc,GACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EACL,sBAAsB,EACtB,KAAK,6BAA6B,GACnC,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,WAAW,EACX,cAAc,EACd,SAAS,EACT,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,EAClB,WAAW,EACX,YAAY,EACZ,KAAK,cAAc,GACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,KAAK,iBAAiB,GACvB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,SAAS,EACT,cAAc,EACd,KAAK,UAAU,GAChB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAGrD,YAAY,EACV,SAAS,EACT,SAAS,EACT,WAAW,EACX,SAAS,EACT,OAAO,EACP,WAAW,EACX,aAAa,EACb,IAAI,EACJ,OAAO,EACP,UAAU,EACV,WAAW,EACX,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,SAAS,EACT,IAAI,EACJ,QAAQ,EACR,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,UAAU,EACV,mBAAmB,EACnB,qBAAqB,EACrB,gBAAgB,EAChB,oBAAoB,EACpB,sBAAsB,EACtB,0BAA0B,EAC1B,uBAAuB,EACvB,oBAAoB,EACpB,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,aAAa,EACb,YAAY,EACZ,kBAAkB,GACnB,MAAM,0BAA0B,CAAC"}
|
package/dist/api/index.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* getToken: () => token,
|
|
27
27
|
* });
|
|
28
28
|
*
|
|
29
|
-
* await client.connect(); // ->
|
|
29
|
+
* await client.connect(); // -> intro_dp_<environment> session cookie
|
|
30
30
|
* const run = await client.tasks.start({
|
|
31
31
|
* prompt: "Summarize my latest order",
|
|
32
32
|
* runtime_id: runtimeId,
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
export { IntrospectionApiClient, } from "./client.js";
|
|
38
38
|
export { TasksClient, TaskRunsClient, RunHandle, } from "./tasks.js";
|
|
39
39
|
export { ConversationItemsClient, ConversationsClient, FileVersionsClient, FilesClient, SharesClient, } from "@introspection-sdk/http";
|
|
40
|
-
export { BrowserHttpClient } from "./http.js";
|
|
40
|
+
export { BrowserHttpClient, ENVIRONMENT_HEADER, } from "./http.js";
|
|
41
41
|
export { Paginator, cursorPaginate, } from "@introspection-sdk/http";
|
|
42
42
|
export { EventType } from "@introspection-sdk/types";
|
|
43
43
|
export { IntrospectionAPIError, AuthenticationError, InsufficientScopeError, NotFoundError, NetworkError, RunnerExpiredError, } from "@introspection-sdk/types";
|
package/dist/api/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EACL,sBAAsB,GAEvB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,WAAW,EACX,cAAc,EACd,SAAS,GAGV,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,EAClB,WAAW,EACX,YAAY,GAEb,MAAM,yBAAyB,CAAC;AACjC,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EACL,sBAAsB,GAEvB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,WAAW,EACX,cAAc,EACd,SAAS,GAGV,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,EAClB,WAAW,EACX,YAAY,GAEb,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,iBAAiB,EACjB,kBAAkB,GAEnB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,SAAS,EACT,cAAc,GAEf,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AA2CrD,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,aAAa,EACb,YAAY,EACZ,kBAAkB,GACnB,MAAM,0BAA0B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@introspection-sdk/introspection-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Introspection observability SDK for the browser",
|
|
6
6
|
"engines": {
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"@opentelemetry/api-logs": "^0.221.0",
|
|
32
32
|
"@opentelemetry/sdk-logs": "^0.221.0",
|
|
33
33
|
"@opentelemetry/exporter-logs-otlp-http": "^0.221.0",
|
|
34
|
-
"@introspection-sdk/types": "0.
|
|
35
|
-
"@introspection-sdk/http": "0.
|
|
34
|
+
"@introspection-sdk/types": "0.16.0",
|
|
35
|
+
"@introspection-sdk/http": "0.16.0"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"lint": "eslint . --max-warnings 0",
|
package/src/api/client.ts
CHANGED
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
* is needed, resolves its `runtime_id` server-side. The SPA fetches
|
|
15
15
|
* the token through the `getToken` callback.
|
|
16
16
|
* 2. `connect()` redeems the token at the DP `POST /v1/oauth/exchange`
|
|
17
|
-
* for
|
|
17
|
+
* for an HttpOnly session cookie, named for the token's environment lane
|
|
18
|
+
* (`intro_dp_development` / `intro_dp_staging` / `intro_dp_production`).
|
|
19
|
+
* Distinct names mean an app running several lanes holds a live session
|
|
20
|
+
* for each in one browser instead of the newest evicting the last.
|
|
18
21
|
* 3. Every subsequent call rides that cookie (`credentials: "include"`)
|
|
19
22
|
* — `client.tasks.start({ runtime_id })`, `.get(...)`, run streaming,
|
|
20
23
|
* etc.
|
|
@@ -31,6 +34,7 @@ import {
|
|
|
31
34
|
FilesClient,
|
|
32
35
|
SharesClient,
|
|
33
36
|
} from "@introspection-sdk/http";
|
|
37
|
+
import type { Environment } from "@introspection-sdk/types";
|
|
34
38
|
import { TasksClient } from "./tasks.js";
|
|
35
39
|
|
|
36
40
|
type CookieClients = {
|
|
@@ -59,11 +63,29 @@ export interface IntrospectionApiClientOptions {
|
|
|
59
63
|
fetch?: typeof fetch;
|
|
60
64
|
/** Extra headers merged into every DP request. */
|
|
61
65
|
additionalHeaders?: Record<string, string>;
|
|
66
|
+
/**
|
|
67
|
+
* Environment lane this client talks to.
|
|
68
|
+
*
|
|
69
|
+
* Normally omit it. `connect()` learns the lane from the exchange response,
|
|
70
|
+
* which the DP derives from the token's own `environment` claim — that is the
|
|
71
|
+
* authoritative source, and it is why a client cannot simply declare a lane
|
|
72
|
+
* it has no token for.
|
|
73
|
+
*
|
|
74
|
+
* Set it only to send the lane header before the first `connect()` resolves.
|
|
75
|
+
* If it disagrees with the token, `connect()` throws rather than silently
|
|
76
|
+
* preferring one.
|
|
77
|
+
*/
|
|
78
|
+
environment?: Environment;
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
export class IntrospectionApiClient {
|
|
65
82
|
private readonly fetchImpl: typeof fetch;
|
|
66
83
|
private readonly cookieClients: CookieClients;
|
|
84
|
+
/**
|
|
85
|
+
* Lane established by the most recent exchange. Per instance, never module
|
|
86
|
+
* scope — two clients on one page holding different lanes is the point.
|
|
87
|
+
*/
|
|
88
|
+
private resolvedEnvironment?: Environment;
|
|
67
89
|
|
|
68
90
|
constructor(private readonly opts: IntrospectionApiClientOptions) {
|
|
69
91
|
// Native browser `fetch` throws "Illegal invocation" when called as a
|
|
@@ -74,11 +96,13 @@ export class IntrospectionApiClient {
|
|
|
74
96
|
if (!opts.dpUrl) {
|
|
75
97
|
throw new Error("IntrospectionApiClient requires a dpUrl");
|
|
76
98
|
}
|
|
99
|
+
this.resolvedEnvironment = opts.environment;
|
|
77
100
|
const http = new BrowserHttpClient({
|
|
78
101
|
apiUrl: opts.dpUrl,
|
|
79
102
|
additionalHeaders: opts.additionalHeaders,
|
|
80
103
|
fetch: this.fetchImpl,
|
|
81
104
|
onUnauthorized: () => this.reexchange(),
|
|
105
|
+
environment: () => this.resolvedEnvironment,
|
|
82
106
|
});
|
|
83
107
|
this.cookieClients = {
|
|
84
108
|
tasks: new TasksClient(http),
|
|
@@ -109,8 +133,17 @@ export class IntrospectionApiClient {
|
|
|
109
133
|
}
|
|
110
134
|
|
|
111
135
|
/**
|
|
112
|
-
*
|
|
113
|
-
* `
|
|
136
|
+
* Environment lane this client is connected to, once `connect()` has
|
|
137
|
+
* resolved. `undefined` before the first exchange, unless the constructor
|
|
138
|
+
* was given one.
|
|
139
|
+
*/
|
|
140
|
+
get environment(): Environment | undefined {
|
|
141
|
+
return this.resolvedEnvironment;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Mint a token via `getToken` and redeem it at the DP for the session
|
|
146
|
+
* cookie. Call once before issuing task requests.
|
|
114
147
|
*/
|
|
115
148
|
async connect(): Promise<void> {
|
|
116
149
|
await this.exchange();
|
|
@@ -131,6 +164,24 @@ export class IntrospectionApiClient {
|
|
|
131
164
|
},
|
|
132
165
|
);
|
|
133
166
|
if (!res.ok) throw await toApiError(res);
|
|
167
|
+
|
|
168
|
+
// The DP echoes the lane it resolved from the token's claim, so the client
|
|
169
|
+
// learns which cookie it just established without being told. Re-read on
|
|
170
|
+
// every exchange, including the 401 recovery path: if the broker has since
|
|
171
|
+
// switched lanes, a stale value would name a cookie we no longer hold.
|
|
172
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
173
|
+
environment?: Environment;
|
|
174
|
+
};
|
|
175
|
+
if (body.environment) {
|
|
176
|
+
if (this.opts.environment && this.opts.environment !== body.environment) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`IntrospectionApiClient is configured for environment ` +
|
|
179
|
+
`"${this.opts.environment}" but getToken() returned a token for ` +
|
|
180
|
+
`"${body.environment}".`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
this.resolvedEnvironment = body.environment;
|
|
184
|
+
}
|
|
134
185
|
}
|
|
135
186
|
|
|
136
187
|
/** 401 recovery: re-exchange, reporting success so the call can retry. */
|
package/src/api/http.ts
CHANGED
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
* The request/response machinery — URL joining, query building, body
|
|
5
5
|
* serialization, error mapping, AG-UI stream parsing — lives in the shared
|
|
6
6
|
* `@introspection-sdk/http` {@link BaseHttpClient}. This module pins the
|
|
7
|
-
* browser auth strategy on top of it: the DP is reached with
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* browser auth strategy on top of it: the DP is reached with an HttpOnly
|
|
8
|
+
* session cookie minted by `POST /v1/oauth/exchange`, so every request opts
|
|
9
|
+
* into `credentials: "include"` and sends NO bearer token — no Introspection
|
|
10
|
+
* credential ever lives in JavaScript. A 401 (expired session cookie)
|
|
11
|
+
* triggers a single `onUnauthorized` refresh + retry.
|
|
12
|
+
*
|
|
13
|
+
* The session cookie is named for its environment lane
|
|
14
|
+
* (`intro_dp_development` / `intro_dp_staging` / `intro_dp_production`), so an
|
|
15
|
+
* app running several lanes holds a live session for each in one browser. When
|
|
16
|
+
* more than one is present the DP cannot tell which a request means, so we name
|
|
17
|
+
* it in {@link ENVIRONMENT_HEADER}.
|
|
12
18
|
*/
|
|
13
19
|
|
|
14
20
|
import {
|
|
@@ -17,11 +23,22 @@ import {
|
|
|
17
23
|
toApiError,
|
|
18
24
|
} from "@introspection-sdk/http";
|
|
19
25
|
|
|
26
|
+
import type { Environment } from "@introspection-sdk/types";
|
|
27
|
+
|
|
20
28
|
import { resolveBrowserFetch } from "./fetch.js";
|
|
21
29
|
|
|
22
30
|
// Re-exported for `client.ts`, which redeems the DP session on `connect()`.
|
|
23
31
|
export { stripTrailingSlash, toApiError };
|
|
24
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Names which environment lane's session cookie the DP should resolve.
|
|
35
|
+
*
|
|
36
|
+
* A selector, not a credential: the cookie it names is still validated
|
|
37
|
+
* server-side, so naming a lane this browser holds no cookie for resolves to
|
|
38
|
+
* nothing rather than granting anything.
|
|
39
|
+
*/
|
|
40
|
+
export const ENVIRONMENT_HEADER = "x-introspection-environment";
|
|
41
|
+
|
|
25
42
|
export interface BrowserHttpConfig {
|
|
26
43
|
/** DP REST base URL every request is prefixed with. */
|
|
27
44
|
apiUrl: string;
|
|
@@ -35,6 +52,14 @@ export interface BrowserHttpConfig {
|
|
|
35
52
|
* `false` to surface the original error.
|
|
36
53
|
*/
|
|
37
54
|
onUnauthorized?: () => Promise<boolean>;
|
|
55
|
+
/**
|
|
56
|
+
* Reads the lane established by the most recent exchange.
|
|
57
|
+
*
|
|
58
|
+
* Called per request rather than captured at construction, because the lane
|
|
59
|
+
* is not known until `connect()` resolves — and it can change under a
|
|
60
|
+
* re-exchange.
|
|
61
|
+
*/
|
|
62
|
+
environment?: () => Environment | undefined;
|
|
38
63
|
}
|
|
39
64
|
|
|
40
65
|
/**
|
|
@@ -49,7 +74,12 @@ export class BrowserHttpClient extends BaseHttpClient {
|
|
|
49
74
|
fetch: resolveBrowserFetch(cfg.fetch),
|
|
50
75
|
additionalHeaders: cfg.additionalHeaders,
|
|
51
76
|
transport: {
|
|
52
|
-
|
|
77
|
+
// Still no bearer token — the HttpOnly cookie is the credential. The
|
|
78
|
+
// only per-request contribution is which lane's cookie to resolve.
|
|
79
|
+
authHeaders: (): Record<string, string> => {
|
|
80
|
+
const environment = cfg.environment?.();
|
|
81
|
+
return environment ? { [ENVIRONMENT_HEADER]: environment } : {};
|
|
82
|
+
},
|
|
53
83
|
credentials: "include",
|
|
54
84
|
onUnauthorized: cfg.onUnauthorized,
|
|
55
85
|
},
|
package/src/api/index.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* getToken: () => token,
|
|
27
27
|
* });
|
|
28
28
|
*
|
|
29
|
-
* await client.connect(); // ->
|
|
29
|
+
* await client.connect(); // -> intro_dp_<environment> session cookie
|
|
30
30
|
* const run = await client.tasks.start({
|
|
31
31
|
* prompt: "Summarize my latest order",
|
|
32
32
|
* runtime_id: runtimeId,
|
|
@@ -54,7 +54,11 @@ export {
|
|
|
54
54
|
SharesClient,
|
|
55
55
|
type FileUploadBody,
|
|
56
56
|
} from "@introspection-sdk/http";
|
|
57
|
-
export {
|
|
57
|
+
export {
|
|
58
|
+
BrowserHttpClient,
|
|
59
|
+
ENVIRONMENT_HEADER,
|
|
60
|
+
type BrowserHttpConfig,
|
|
61
|
+
} from "./http.js";
|
|
58
62
|
export {
|
|
59
63
|
Paginator,
|
|
60
64
|
cursorPaginate,
|
|
@@ -66,6 +70,7 @@ export { EventType } from "@introspection-sdk/types";
|
|
|
66
70
|
export type {
|
|
67
71
|
AGUIEvent,
|
|
68
72
|
BaseEvent,
|
|
73
|
+
Environment,
|
|
69
74
|
Interrupt,
|
|
70
75
|
Message,
|
|
71
76
|
ResumeEntry,
|