agentfootprint 9.7.0 → 9.9.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/dist/adapters/identity/vault.js +336 -0
- package/dist/adapters/identity/vault.js.map +1 -0
- package/dist/adapters/observability/audit.js +2 -28
- package/dist/adapters/observability/audit.js.map +1 -1
- package/dist/adapters/observability/file.js +307 -0
- package/dist/adapters/observability/file.js.map +1 -0
- package/dist/adapters/observability/githubBugReporter.js +461 -0
- package/dist/adapters/observability/githubBugReporter.js.map +1 -0
- package/dist/adapters/observability/githubDeviceSignIn.js +263 -0
- package/dist/adapters/observability/githubDeviceSignIn.js.map +1 -0
- package/dist/esm/adapters/identity/vault.d.ts +146 -0
- package/dist/esm/adapters/identity/vault.js +332 -0
- package/dist/esm/adapters/identity/vault.js.map +1 -0
- package/dist/esm/adapters/observability/audit.js +1 -27
- package/dist/esm/adapters/observability/audit.js.map +1 -1
- package/dist/esm/adapters/observability/file.d.ts +145 -0
- package/dist/esm/adapters/observability/file.js +303 -0
- package/dist/esm/adapters/observability/file.js.map +1 -0
- package/dist/esm/adapters/observability/githubBugReporter.d.ts +154 -0
- package/dist/esm/adapters/observability/githubBugReporter.js +457 -0
- package/dist/esm/adapters/observability/githubBugReporter.js.map +1 -0
- package/dist/esm/adapters/observability/githubDeviceSignIn.d.ts +131 -0
- package/dist/esm/adapters/observability/githubDeviceSignIn.js +259 -0
- package/dist/esm/adapters/observability/githubDeviceSignIn.js.map +1 -0
- package/dist/esm/identity.d.ts +1 -0
- package/dist/esm/identity.js +4 -0
- package/dist/esm/identity.js.map +1 -1
- package/dist/esm/lib/bug-report/build.d.ts +103 -0
- package/dist/esm/lib/bug-report/build.js +648 -0
- package/dist/esm/lib/bug-report/build.js.map +1 -0
- package/dist/esm/lib/bug-report/index.d.ts +14 -0
- package/dist/esm/lib/bug-report/index.js +13 -0
- package/dist/esm/lib/bug-report/index.js.map +1 -0
- package/dist/esm/lib/bug-report/transcript.d.ts +61 -0
- package/dist/esm/lib/bug-report/transcript.js +124 -0
- package/dist/esm/lib/bug-report/transcript.js.map +1 -0
- package/dist/esm/lib/bug-report/types.d.ts +206 -0
- package/dist/esm/lib/bug-report/types.js +12 -0
- package/dist/esm/lib/bug-report/types.js.map +1 -0
- package/dist/esm/lib/bug-report/zip.d.ts +71 -0
- package/dist/esm/lib/bug-report/zip.js +202 -0
- package/dist/esm/lib/bug-report/zip.js.map +1 -0
- package/dist/esm/lib/libraryVersion.d.ts +23 -0
- package/dist/esm/lib/libraryVersion.js +46 -0
- package/dist/esm/lib/libraryVersion.js.map +1 -0
- package/dist/esm/lib/trace-toolpack/openRecording.d.ts +17 -0
- package/dist/esm/lib/trace-toolpack/openRecording.js +8 -2
- package/dist/esm/lib/trace-toolpack/openRecording.js.map +1 -1
- package/dist/esm/observability-providers.d.ts +6 -1
- package/dist/esm/observability-providers.js +16 -1
- package/dist/esm/observability-providers.js.map +1 -1
- package/dist/esm/observe.d.ts +1 -0
- package/dist/esm/observe.js +6 -0
- package/dist/esm/observe.js.map +1 -1
- package/dist/identity.js +6 -1
- package/dist/identity.js.map +1 -1
- package/dist/lib/bug-report/build.js +656 -0
- package/dist/lib/bug-report/build.js.map +1 -0
- package/dist/lib/bug-report/index.js +18 -0
- package/dist/lib/bug-report/index.js.map +1 -0
- package/dist/lib/bug-report/transcript.js +128 -0
- package/dist/lib/bug-report/transcript.js.map +1 -0
- package/dist/lib/bug-report/types.js +13 -0
- package/dist/lib/bug-report/types.js.map +1 -0
- package/dist/lib/bug-report/zip.js +207 -0
- package/dist/lib/bug-report/zip.js.map +1 -0
- package/dist/lib/libraryVersion.js +51 -0
- package/dist/lib/libraryVersion.js.map +1 -0
- package/dist/lib/trace-toolpack/openRecording.js +9 -2
- package/dist/lib/trace-toolpack/openRecording.js.map +1 -1
- package/dist/observability-providers.js +20 -2
- package/dist/observability-providers.js.map +1 -1
- package/dist/observe.js +14 -6
- package/dist/observe.js.map +1 -1
- package/dist/types/adapters/identity/vault.d.ts +147 -0
- package/dist/types/adapters/identity/vault.d.ts.map +1 -0
- package/dist/types/adapters/observability/audit.d.ts.map +1 -1
- package/dist/types/adapters/observability/file.d.ts +146 -0
- package/dist/types/adapters/observability/file.d.ts.map +1 -0
- package/dist/types/adapters/observability/githubBugReporter.d.ts +155 -0
- package/dist/types/adapters/observability/githubBugReporter.d.ts.map +1 -0
- package/dist/types/adapters/observability/githubDeviceSignIn.d.ts +132 -0
- package/dist/types/adapters/observability/githubDeviceSignIn.d.ts.map +1 -0
- package/dist/types/identity.d.ts +1 -0
- package/dist/types/identity.d.ts.map +1 -1
- package/dist/types/lib/bug-report/build.d.ts +104 -0
- package/dist/types/lib/bug-report/build.d.ts.map +1 -0
- package/dist/types/lib/bug-report/index.d.ts +15 -0
- package/dist/types/lib/bug-report/index.d.ts.map +1 -0
- package/dist/types/lib/bug-report/transcript.d.ts +62 -0
- package/dist/types/lib/bug-report/transcript.d.ts.map +1 -0
- package/dist/types/lib/bug-report/types.d.ts +207 -0
- package/dist/types/lib/bug-report/types.d.ts.map +1 -0
- package/dist/types/lib/bug-report/zip.d.ts +72 -0
- package/dist/types/lib/bug-report/zip.d.ts.map +1 -0
- package/dist/types/lib/libraryVersion.d.ts +24 -0
- package/dist/types/lib/libraryVersion.d.ts.map +1 -0
- package/dist/types/lib/trace-toolpack/openRecording.d.ts +17 -0
- package/dist/types/lib/trace-toolpack/openRecording.d.ts.map +1 -1
- package/dist/types/observability-providers.d.ts +6 -1
- package/dist/types/observability-providers.d.ts.map +1 -1
- package/dist/types/observe.d.ts +1 -0
- package/dist/types/observe.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* githubDeviceSignIn — let the REPORTER sign in, so the issue is filed as them.
|
|
3
|
+
*
|
|
4
|
+
* import { githubDeviceSignIn } from 'agentfootprint/observe';
|
|
5
|
+
*
|
|
6
|
+
* const signIn = await githubDeviceSignIn({ clientId: 'Iv1.0123456789abcdef' });
|
|
7
|
+
* show(`Go to ${signIn.verificationUri} and enter ${signIn.userCode}`);
|
|
8
|
+
* const { token, login } = await signIn.completed; // resolves on authorize
|
|
9
|
+
*
|
|
10
|
+
* GitHub's OAuth **device flow**, spoken as three plain `fetch` calls and
|
|
11
|
+
* nothing else: request a code, show it to the human, poll until they approve.
|
|
12
|
+
* Zero dependencies, and it runs in a browser as well as on a server — there is
|
|
13
|
+
* no client secret in this flow, which is exactly why it is the one that works
|
|
14
|
+
* from a page.
|
|
15
|
+
*
|
|
16
|
+
* ## Why this exists beside a server token
|
|
17
|
+
*
|
|
18
|
+
* A server-side PAT files every report as the application. That is right for an
|
|
19
|
+
* automated "Report a problem" button: the app owns the repo, the app owns the
|
|
20
|
+
* token. It is wrong when the value is ATTRIBUTION — a field tester filing
|
|
21
|
+
* upstream should appear as themselves, so a maintainer can ask them a
|
|
22
|
+
* follow-up question and so their report counts as theirs.
|
|
23
|
+
*
|
|
24
|
+
* | | server PAT | device sign-in |
|
|
25
|
+
* |---|---|---|
|
|
26
|
+
* | Who the issue is from | the application | the reporter |
|
|
27
|
+
* | Where the token lives | server environment | the reporter's session, in memory |
|
|
28
|
+
* | Human steps | none | one: enter a code, approve |
|
|
29
|
+
* | Right for | in-app "report a problem" | field testers filing upstream |
|
|
30
|
+
*
|
|
31
|
+
* The token this returns is handed to {@link githubBugReporter} as `token`,
|
|
32
|
+
* with no special-casing anywhere: a token is a token.
|
|
33
|
+
*
|
|
34
|
+
* ## Keep it in memory, for the session only
|
|
35
|
+
*
|
|
36
|
+
* **Never `localStorage`, never a cookie, never a log line.** A device-flow
|
|
37
|
+
* token is a live credential for the account that approved it; persisting it in
|
|
38
|
+
* a browser turns one XSS into a lasting account compromise. Hold it in a
|
|
39
|
+
* variable, use it, drop it when the tab closes.
|
|
40
|
+
*
|
|
41
|
+
* ## Scopes are coarse here, and that is GitHub's design
|
|
42
|
+
*
|
|
43
|
+
* The device flow issues a CLASSIC OAuth token, and classic scopes are coarse:
|
|
44
|
+
* `public_repo` (the default here) grants write across every public repository
|
|
45
|
+
* the account can reach, and `repo` grants it across private ones too. There is
|
|
46
|
+
* no fine-grained equivalent in this flow. That trade buys attribution — the
|
|
47
|
+
* issue is really from that person — and it is the reason the default stops at
|
|
48
|
+
* `public_repo`: filing an issue needs no more, and the token disappears with
|
|
49
|
+
* the session. Where least privilege matters more than attribution, use a
|
|
50
|
+
* fine-grained PAT on a server instead (see {@link githubBugReporter}).
|
|
51
|
+
*
|
|
52
|
+
* ## The collaborator caveat, stated rather than discovered
|
|
53
|
+
*
|
|
54
|
+
* A reporter who signs in as themselves can file an issue on a public repo, and
|
|
55
|
+
* can commit evidence ONLY to a repository they can write to. Pointing
|
|
56
|
+
* `evidenceRepo` at a private repo the reporter is not a collaborator on will
|
|
57
|
+
* fail with a 404 (GitHub hides private repos from tokens that cannot see
|
|
58
|
+
* them). Either add the reporter as a collaborator, or let them attach the zip
|
|
59
|
+
* to the issue by hand — `exportBugReport` gives them the file either way.
|
|
60
|
+
*
|
|
61
|
+
* ## Secrecy
|
|
62
|
+
*
|
|
63
|
+
* The token appears in no message this module can produce. The three failure
|
|
64
|
+
* shapes of the flow — the human denied it, the code expired, the poll was
|
|
65
|
+
* aborted — are named plainly, with GitHub's `error_description` only, never a
|
|
66
|
+
* response body or a request.
|
|
67
|
+
*/
|
|
68
|
+
/** The endpoints, so a GitHub Enterprise Server deployment can move them. */
|
|
69
|
+
const DEFAULT_AUTH_BASE = 'https://github.com';
|
|
70
|
+
const DEFAULT_API_BASE = 'https://api.github.com';
|
|
71
|
+
const defaultSleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
72
|
+
if (signal?.aborted) {
|
|
73
|
+
reject(abortedError());
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const timer = setTimeout(() => {
|
|
77
|
+
signal?.removeEventListener('abort', onAbort);
|
|
78
|
+
resolve();
|
|
79
|
+
}, ms);
|
|
80
|
+
const onAbort = () => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
reject(abortedError());
|
|
83
|
+
};
|
|
84
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
85
|
+
});
|
|
86
|
+
const abortedError = () => new Error('githubDeviceSignIn: sign-in was cancelled before the code was approved.');
|
|
87
|
+
/**
|
|
88
|
+
* Start a device-flow sign-in.
|
|
89
|
+
*
|
|
90
|
+
* Resolves as soon as GitHub hands back a code — that is the point, because
|
|
91
|
+
* the human cannot approve a code they have not been shown. The waiting happens
|
|
92
|
+
* on the returned `completed` promise.
|
|
93
|
+
*
|
|
94
|
+
* @throws TypeError when `clientId` is missing (naming where it comes from).
|
|
95
|
+
* @throws Error naming the status when GitHub refuses to issue a code.
|
|
96
|
+
*/
|
|
97
|
+
export async function githubDeviceSignIn(options) {
|
|
98
|
+
if (typeof options?.clientId !== 'string' || options.clientId.trim() === '') {
|
|
99
|
+
throw new TypeError('githubDeviceSignIn: `clientId` is required. Create an OAuth App once under your ' +
|
|
100
|
+
'organisation (Settings → Developer settings → OAuth Apps), tick "Enable Device ' +
|
|
101
|
+
'Flow", and pass its Client ID here. It is public by design — the device flow has no ' +
|
|
102
|
+
'client secret, which is why it can run in a browser.');
|
|
103
|
+
}
|
|
104
|
+
const authBase = (options.authBase ?? DEFAULT_AUTH_BASE).replace(/\/+$/, '');
|
|
105
|
+
const apiBase = (options.apiBase ?? DEFAULT_API_BASE).replace(/\/+$/, '');
|
|
106
|
+
const doFetch = options._fetch ?? ((...args) => fetch(...args));
|
|
107
|
+
const sleep = options._sleep ?? defaultSleep;
|
|
108
|
+
const scopes = options.scopes ?? ['public_repo'];
|
|
109
|
+
const start = await postForm({
|
|
110
|
+
doFetch,
|
|
111
|
+
url: `${authBase}/login/device/code`,
|
|
112
|
+
what: 'request a device code',
|
|
113
|
+
form: { client_id: options.clientId, scope: scopes.join(' ') },
|
|
114
|
+
});
|
|
115
|
+
const deviceCode = stringField(start, 'device_code');
|
|
116
|
+
const userCode = stringField(start, 'user_code');
|
|
117
|
+
const verificationUri = stringField(start, 'verification_uri');
|
|
118
|
+
if (!deviceCode || !userCode || !verificationUri) {
|
|
119
|
+
throw new Error(`githubDeviceSignIn: GitHub did not return a device code${errorSuffix(start)}. Check that ` +
|
|
120
|
+
'the OAuth App exists and has Device Flow enabled.');
|
|
121
|
+
}
|
|
122
|
+
const expiresIn = numberField(start, 'expires_in') ?? 900;
|
|
123
|
+
const interval = numberField(start, 'interval') ?? 5;
|
|
124
|
+
const completed = pollForToken({
|
|
125
|
+
doFetch,
|
|
126
|
+
sleep,
|
|
127
|
+
authBase,
|
|
128
|
+
apiBase,
|
|
129
|
+
clientId: options.clientId,
|
|
130
|
+
deviceCode,
|
|
131
|
+
interval,
|
|
132
|
+
expiresIn,
|
|
133
|
+
...(options.signal !== undefined && { signal: options.signal }),
|
|
134
|
+
});
|
|
135
|
+
// Nobody has to await `completed` for polling to run; make sure an unawaited
|
|
136
|
+
// rejection is not an unhandled one in the meantime.
|
|
137
|
+
completed.catch(() => undefined);
|
|
138
|
+
return { userCode, verificationUri, expiresIn, interval, completed };
|
|
139
|
+
}
|
|
140
|
+
async function pollForToken(args) {
|
|
141
|
+
let waitSeconds = args.interval;
|
|
142
|
+
const deadline = Date.now() + args.expiresIn * 1000;
|
|
143
|
+
for (;;) {
|
|
144
|
+
if (args.signal?.aborted)
|
|
145
|
+
throw abortedError();
|
|
146
|
+
await args.sleep(waitSeconds * 1000, args.signal);
|
|
147
|
+
if (Date.now() > deadline) {
|
|
148
|
+
throw new Error(`githubDeviceSignIn: the code expired after ${args.expiresIn}s without being ` +
|
|
149
|
+
'approved. Start the sign-in again to get a fresh one.');
|
|
150
|
+
}
|
|
151
|
+
const body = await postForm({
|
|
152
|
+
doFetch: args.doFetch,
|
|
153
|
+
url: `${args.authBase}/login/oauth/access_token`,
|
|
154
|
+
what: 'exchange the device code',
|
|
155
|
+
form: {
|
|
156
|
+
client_id: args.clientId,
|
|
157
|
+
device_code: args.deviceCode,
|
|
158
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
const token = stringField(body, 'access_token');
|
|
162
|
+
if (token) {
|
|
163
|
+
const scope = stringField(body, 'scope') ?? '';
|
|
164
|
+
const login = await readLogin(args.doFetch, args.apiBase, token);
|
|
165
|
+
return {
|
|
166
|
+
token,
|
|
167
|
+
tokenType: stringField(body, 'token_type') ?? 'bearer',
|
|
168
|
+
scopes: scope ? scope.split(/[\s,]+/).filter(Boolean) : [],
|
|
169
|
+
...(login !== undefined && { login }),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const error = stringField(body, 'error');
|
|
173
|
+
switch (error) {
|
|
174
|
+
case 'authorization_pending':
|
|
175
|
+
// The human has not finished yet. This is the normal answer.
|
|
176
|
+
break;
|
|
177
|
+
case 'slow_down':
|
|
178
|
+
// GitHub's own back-pressure: it tells us the new interval, and polling
|
|
179
|
+
// faster than it asked gets the flow rate-limited out entirely.
|
|
180
|
+
waitSeconds = numberField(body, 'interval') ?? waitSeconds + 5;
|
|
181
|
+
break;
|
|
182
|
+
case 'expired_token':
|
|
183
|
+
throw new Error('githubDeviceSignIn: the code expired before it was approved. Start the sign-in ' +
|
|
184
|
+
'again to get a fresh one.');
|
|
185
|
+
case 'access_denied':
|
|
186
|
+
throw new Error('githubDeviceSignIn: the sign-in was denied on GitHub, so no token was issued. ' +
|
|
187
|
+
'Nothing has been filed.');
|
|
188
|
+
default:
|
|
189
|
+
throw new Error(`githubDeviceSignIn: GitHub refused the device code exchange${errorSuffix(body)}.`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** Attribution: which login will the issue read as? Never fatal — a token that
|
|
194
|
+
* cannot read `/user` can still file an issue. */
|
|
195
|
+
async function readLogin(doFetch, apiBase, token) {
|
|
196
|
+
try {
|
|
197
|
+
const res = await doFetch(`${apiBase}/user`, {
|
|
198
|
+
headers: {
|
|
199
|
+
authorization: `Bearer ${token}`,
|
|
200
|
+
accept: 'application/vnd.github+json',
|
|
201
|
+
'x-github-api-version': '2022-11-28',
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
if (!res.ok)
|
|
205
|
+
return undefined;
|
|
206
|
+
const body = (await res.json());
|
|
207
|
+
return typeof body?.login === 'string' ? body.login : undefined;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* One `application/x-www-form-urlencoded` POST that asks for JSON back.
|
|
215
|
+
*
|
|
216
|
+
* The device flow's endpoints answer form-encoded by default; `accept: json` is
|
|
217
|
+
* what makes them speak JSON. A thrown fetch is re-wrapped so no implementation
|
|
218
|
+
* can put the request — which carries the device code, and later the token — in
|
|
219
|
+
* the message.
|
|
220
|
+
*/
|
|
221
|
+
async function postForm(args) {
|
|
222
|
+
let res;
|
|
223
|
+
try {
|
|
224
|
+
res = await args.doFetch(args.url, {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
headers: { accept: 'application/json', 'content-type': 'application/x-www-form-urlencoded' },
|
|
227
|
+
body: new URLSearchParams(args.form).toString(),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
throw new Error(`githubDeviceSignIn: could not reach GitHub to ${args.what} ` +
|
|
232
|
+
`(${err instanceof Error ? err.name || 'network error' : 'network error'}).`);
|
|
233
|
+
}
|
|
234
|
+
let body;
|
|
235
|
+
try {
|
|
236
|
+
body = await res.json();
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
body = undefined;
|
|
240
|
+
}
|
|
241
|
+
const record = typeof body === 'object' && body !== null ? body : {};
|
|
242
|
+
// A 4xx/5xx with no parsed error field is all we may say: the response body
|
|
243
|
+
// of this endpoint can echo the request.
|
|
244
|
+
if (!res.ok && typeof record.error !== 'string') {
|
|
245
|
+
throw new Error(`githubDeviceSignIn: GitHub answered ${res.status} when asked to ${args.what}.`);
|
|
246
|
+
}
|
|
247
|
+
return record;
|
|
248
|
+
}
|
|
249
|
+
const stringField = (body, key) => typeof body[key] === 'string' && body[key] !== '' ? body[key] : undefined;
|
|
250
|
+
const numberField = (body, key) => typeof body[key] === 'number' ? body[key] : undefined;
|
|
251
|
+
/** GitHub's `error` / `error_description` — the only response text we echo. */
|
|
252
|
+
function errorSuffix(body) {
|
|
253
|
+
const code = stringField(body, 'error');
|
|
254
|
+
const description = stringField(body, 'error_description');
|
|
255
|
+
if (!code && !description)
|
|
256
|
+
return '';
|
|
257
|
+
return ` (${[code, description].filter(Boolean).join(': ').slice(0, 200)})`;
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=githubDeviceSignIn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"githubDeviceSignIn.js","sourceRoot":"","sources":["../../../../src/adapters/observability/githubDeviceSignIn.ts"],"names":[],"mappings":"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"}
|
package/dist/esm/identity.d.ts
CHANGED
|
@@ -35,3 +35,4 @@ export type { AuthorizationRequiredMode, ConsentRequest } from './identity/conse
|
|
|
35
35
|
export { CredentialConsentRequiredError, type CredentialConsentRequiredContext, } from './identity/CredentialConsentRequiredError.js';
|
|
36
36
|
export { withCredentialRetry, type WithCredentialRetryOptions, } from './identity/withCredentialRetry.js';
|
|
37
37
|
export { agentCoreIdentity, type AgentCoreIdentityOptions, type AgentCoreIdentityClientLike, type AgentCoreOauthResponse, } from './adapters/identity/agentcore.js';
|
|
38
|
+
export { vaultCredentials, type VaultCredentialsOptions } from './adapters/identity/vault.js';
|
package/dist/esm/identity.js
CHANGED
|
@@ -33,4 +33,8 @@ export { staticTokens } from './identity/staticTokens.js';
|
|
|
33
33
|
export { CredentialConsentRequiredError, } from './identity/CredentialConsentRequiredError.js';
|
|
34
34
|
export { withCredentialRetry, } from './identity/withCredentialRetry.js';
|
|
35
35
|
export { agentCoreIdentity, } from './adapters/identity/agentcore.js';
|
|
36
|
+
// HashiCorp-Vault-compatible KV v2, over plain HTTP (9.8.0) — no SDK, no
|
|
37
|
+
// vendor client. V1 is token auth + KV v2 + no leases, and every other shape
|
|
38
|
+
// is refused by name rather than guessed at; see the module docstring.
|
|
39
|
+
export { vaultCredentials } from './adapters/identity/vault.js';
|
|
36
40
|
//# sourceMappingURL=identity.js.map
|
package/dist/esm/identity.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAWH,OAAO,EAAE,kBAAkB,EAAE,8BAA8B,EAAE,MAAM,qBAAqB,CAAC;AACzF,OAAO,EACL,MAAM,EACN,MAAM,EACN,KAAK,EACL,OAAO,GAKR,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAA4B,MAAM,4BAA4B,CAAC;AAKpF,OAAO,EACL,8BAA8B,GAE/B,MAAM,8CAA8C,CAAC;AACtD,OAAO,EACL,mBAAmB,GAEpB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,iBAAiB,GAIlB,MAAM,kCAAkC,CAAC"}
|
|
1
|
+
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAWH,OAAO,EAAE,kBAAkB,EAAE,8BAA8B,EAAE,MAAM,qBAAqB,CAAC;AACzF,OAAO,EACL,MAAM,EACN,MAAM,EACN,KAAK,EACL,OAAO,GAKR,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAA4B,MAAM,4BAA4B,CAAC;AAKpF,OAAO,EACL,8BAA8B,GAE/B,MAAM,8CAA8C,CAAC;AACtD,OAAO,EACL,mBAAmB,GAEpB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,iBAAiB,GAIlB,MAAM,kCAAkC,CAAC;AAC1C,yEAAyE;AACzE,6EAA6E;AAC7E,uEAAuE;AACvE,OAAO,EAAE,gBAAgB,EAAgC,MAAM,8BAA8B,CAAC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* exportBugReport / describeBugReport — a bug report IS the evidence.
|
|
3
|
+
*
|
|
4
|
+
* The usual bug report is a person's memory of a run: "it said the wrong
|
|
5
|
+
* thing, I think it called the search tool twice". The run itself — the
|
|
6
|
+
* timeline, the state, the chart, the narrative — is sitting right there in
|
|
7
|
+
* the process and never leaves it. This turns that around: the report is the
|
|
8
|
+
* run, packaged, with the prose attached.
|
|
9
|
+
*
|
|
10
|
+
* ## Two calls, because consent needs two
|
|
11
|
+
*
|
|
12
|
+
* 1. {@link describeBugReport} — "here is what would be sent." A manifest of
|
|
13
|
+
* SELECTABLE UNITS: each conversation with its event and turn counts and
|
|
14
|
+
* its size, each derived file, the redacted keys by name, the total, and
|
|
15
|
+
* a loud warning with trim hints if it is too big.
|
|
16
|
+
* 2. {@link exportBugReport} — "send exactly these." The reporter's ticked
|
|
17
|
+
* unit ids come back as `include`, and the bundle carries only those.
|
|
18
|
+
*
|
|
19
|
+
* One call does work for a server-side reporter that has no human in front of
|
|
20
|
+
* it; the two-call shape is what makes a browser consent dialog possible at
|
|
21
|
+
* all, because a dialog cannot ask about a blob it has not measured.
|
|
22
|
+
*
|
|
23
|
+
* ## What is in the bundle
|
|
24
|
+
*
|
|
25
|
+
* | file | what it is |
|
|
26
|
+
* |---|---|
|
|
27
|
+
* | `manifest.json` | this manifest — always present, never a selectable unit |
|
|
28
|
+
* | `recording.json` | the canon `{ snapshot, events, structure }` — drops straight into `observeRecording()` |
|
|
29
|
+
* | `conversations/<id>.json` | one file per conversation, when there is more than one run |
|
|
30
|
+
* | `conversation.json` | the readable transcript, derived from the events |
|
|
31
|
+
* | `narrative.txt` | the narrative recorder's lines, when one was attached |
|
|
32
|
+
* | `environment.json` | versions + the reporter's prose |
|
|
33
|
+
*
|
|
34
|
+
* `environment.json` is deliberately the whole environment: library version,
|
|
35
|
+
* engine version, Node version, platform and architecture. **No username, no
|
|
36
|
+
* hostname, no working directory, no environment variables, no file paths.**
|
|
37
|
+
* A bug report should not be the way an internal directory layout leaves a
|
|
38
|
+
* company.
|
|
39
|
+
*
|
|
40
|
+
* ## Redaction is already done, and the manifest proves it
|
|
41
|
+
*
|
|
42
|
+
* The recording arrives ALREADY redacted: footprintjs scrubs at commit time
|
|
43
|
+
* under the run's `RedactionPolicy`, so a redacted value was never in the
|
|
44
|
+
* snapshot this reads. Nothing here scrubs anything — it would be too late to
|
|
45
|
+
* matter and a second policy could only disagree with the first. What this
|
|
46
|
+
* does do is LIST the redacted keys by name, derived from the placeholders
|
|
47
|
+
* actually present in the evidence, so a human consenting to the bundle can
|
|
48
|
+
* see which secrets were protected. A key that is not on that list was not
|
|
49
|
+
* redacted, and the honest reading of an empty list is "no policy was set" —
|
|
50
|
+
* which the manifest says in a note.
|
|
51
|
+
*
|
|
52
|
+
* @example The consent flow
|
|
53
|
+
* ```ts
|
|
54
|
+
* const manifest = describeBugReport(recording);
|
|
55
|
+
* // …show manifest.units to the human; they tick some…
|
|
56
|
+
* const report = exportBugReport(recording, {
|
|
57
|
+
* include: ['conv-1', 'file-narrative', 'file-environment'],
|
|
58
|
+
* title: 'Agent answered with a stale price',
|
|
59
|
+
* stepsToReproduce: '1. ask for the price\n2. update it\n3. ask again',
|
|
60
|
+
* expected: 'the new price',
|
|
61
|
+
* actual: 'the old one',
|
|
62
|
+
* });
|
|
63
|
+
* fs.writeFileSync(report.filename, report.zip);
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
import type { BugReport, BugReportInput, BugReportManifest, DescribeBugReportOptions, ExportBugReportOptions } from './types.js';
|
|
67
|
+
/**
|
|
68
|
+
* A size a human reads. KB under a megabyte, MB above it — a ceiling reported
|
|
69
|
+
* as "0.0 MB" teaches nothing, and these strings are the whole content of a
|
|
70
|
+
* refusal.
|
|
71
|
+
*/
|
|
72
|
+
export declare function formatBytes(bytes: number): string;
|
|
73
|
+
/**
|
|
74
|
+
* Measure a bug report before anything leaves — the consent step.
|
|
75
|
+
*
|
|
76
|
+
* Every unit is "selected" in the manifest this returns, because nothing has
|
|
77
|
+
* been chosen yet: it is the offer, with sizes and counts attached, for a human
|
|
78
|
+
* (or a policy) to narrow. Show `units` to the reporter; pass the ids they keep
|
|
79
|
+
* to {@link exportBugReport} as `include`.
|
|
80
|
+
*
|
|
81
|
+
* Cheap enough to call on every dialog open: it serializes the files to measure
|
|
82
|
+
* them, and throws them away.
|
|
83
|
+
*
|
|
84
|
+
* @param input a recording, a `recordRun` handle, a runner, or an array.
|
|
85
|
+
* @param options size ceiling and a fixed timestamp.
|
|
86
|
+
*/
|
|
87
|
+
export declare function describeBugReport(input: BugReportInput, options?: DescribeBugReportOptions): BugReportManifest;
|
|
88
|
+
/**
|
|
89
|
+
* Build the bundle: the manifest, the named files, and a real zip of them.
|
|
90
|
+
*
|
|
91
|
+
* @param input a recording, a `recordRun` handle, a runner, or an array.
|
|
92
|
+
* @param options the reporter's prose, plus `include` — the ids from
|
|
93
|
+
* {@link describeBugReport} that the reporter consented to.
|
|
94
|
+
*
|
|
95
|
+
* @throws TypeError naming the unknown id when `include` names a unit that
|
|
96
|
+
* does not exist, and naming the available conversations when the
|
|
97
|
+
* selection would carry no evidence at all.
|
|
98
|
+
*/
|
|
99
|
+
export declare function exportBugReport(input: BugReportInput, options: ExportBugReportOptions): BugReport;
|
|
100
|
+
/** `2026-08-11-agent-answered-with-a-stale-price.zip`. */
|
|
101
|
+
export declare function bundleFilename(title: string, createdAt: Date): string;
|
|
102
|
+
/** Lower-case, ASCII, hyphenated, bounded — a filename, not a sentence. */
|
|
103
|
+
export declare function slugify(title: string): string;
|