@byokit/accounts 0.5.0 → 0.7.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/CHANGELOG.md +13 -1
- package/README.md +15 -0
- package/dist/accounts.d.ts +2 -0
- package/dist/accounts.js +1 -1
- package/dist/chatgpt-plan.d.ts +21 -0
- package/dist/chatgpt-plan.js +20 -0
- package/dist/portable.d.ts +1 -0
- package/dist/portable.js +1 -0
- package/dist/responses.d.ts +5 -1
- package/dist/responses.js +18 -11
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
## 0.
|
|
5
|
+
## 0.7.0 (2026-09-30)
|
|
6
|
+
|
|
7
|
+
- Add the portable `chatgptPlan` adapter for a host-validated official token-sharing session, checking
|
|
8
|
+
ChatGPT plan usage consent on every access. Uses subscription billing and never falls back to an API key
|
|
9
|
+
(billed per use); the host owns sign-in, identity verification, storage and refresh per person.
|
|
10
|
+
|
|
11
|
+
## 0.6.0 (2026-09-30)
|
|
12
|
+
|
|
13
|
+
- `respond` and `Accounts.respond` accept `originator` (or set it once on `Accounts`): the app's own originator header value. Default: 'byokit', as before.
|
|
14
|
+
- FIX: a garbled streamed answer no longer arrives as an empty string with HTTP 200: a data line the parser cannot read now throws a ResponseError the app can show.
|
|
15
|
+
- FIX: a stream that ends with no words, no completed answer and no tool calls now throws instead of resolving to an empty string.
|
|
16
|
+
- FIX: the answer is the words as they streamed in; the completed envelope is only used when nothing streamed. Apps whose completed envelope carries no text no longer see their streamed words replaced by an empty answer.
|
|
17
|
+
- FIX: streamed answers split on bare-CR line endings too, so a backend that separates events with carriage returns no longer yields an empty answer.
|
|
6
18
|
|
|
7
19
|
- `respond` passes the whole question through: a message array (many turns, pictures with `input_image`, a `function_call` with its `function_call_output`), `tools` and `tool_choice` (the app's own function tools and built-ins, including `image_generation`), how hard the model thinks (`reasoning.effort`), and how long the answer is with the shape it must follow (`text.verbosity`, `text.format`). With `tools` the result is the text with every output item (`isFunctionCall` spots a call); without, the plain text as before. `onEvent` sees each tool call and output item as it streams.
|
|
8
20
|
|
package/README.md
CHANGED
|
@@ -276,3 +276,18 @@ tests and demos sign in and ask end to end with no account. Point the kit at it
|
|
|
276
276
|
## License
|
|
277
277
|
|
|
278
278
|
Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](../../NOTICE).
|
|
279
|
+
|
|
280
|
+
## Official ChatGPT plan token-sharing adapter
|
|
281
|
+
|
|
282
|
+
`chatgptPlan({ session })`, also exported from `@byokit/accounts/chatgpt-plan`, binds a single person's
|
|
283
|
+
validated [official token-sharing session](https://developers.openai.com/siwc/token-sharing-open-source) to a
|
|
284
|
+
consumer such as `@byokit/decide`. `session(signal)` returns `{ accessToken, scopes }` after the host's own
|
|
285
|
+
sign-in integration validates identity and refreshes tokens. The adapter checks `resource.invoke` and
|
|
286
|
+
`chatgpt.tokens.use.direct` on every access; missing consent throws `UnsupportedAccountError` with
|
|
287
|
+
`code: 'unsupported_account'`. Billing is `subscription`, with no API-key fallback.
|
|
288
|
+
|
|
289
|
+
The host follows [official registration/sign-in](https://developers.openai.com/siwc/token-sharing-open-source/sign-in),
|
|
290
|
+
including ID-token signature/issuer/audience/nonce verification and protected per-person storage. Plan usage is
|
|
291
|
+
available to eligible open-source/local apps; paid/remote apps require approval. This adapter does not start an
|
|
292
|
+
OAuth flow and does not convert the existing Codex `Accounts.login()` credential into a token-sharing session.
|
|
293
|
+
It reads no environment or files, and remains portable to browsers and React Native.
|
package/dist/accounts.d.ts
CHANGED
|
@@ -67,6 +67,8 @@ export type AccountsOptions<M extends Member = Member> = {
|
|
|
67
67
|
apiBase?: string;
|
|
68
68
|
/** The fetch `respond` asks with: one that streams on a phone (Expo's `expo/fetch`). Default: the platform's. */
|
|
69
69
|
fetch?: typeof fetch;
|
|
70
|
+
/** The originator header `respond` sends. Default: 'byokit'. */
|
|
71
|
+
originator?: string;
|
|
70
72
|
};
|
|
71
73
|
/** The ChatGPT plan behind a sign-in, from its own token: a work plan (Business, Enterprise, Edu) follows the employer's rules. */
|
|
72
74
|
export declare function planOf(access: string): {
|
package/dist/accounts.js
CHANGED
|
@@ -236,7 +236,7 @@ export class Accounts {
|
|
|
236
236
|
if (!access || c?.type !== 'oauth')
|
|
237
237
|
throw new ResponseError(say('status.signedOut', { name: p.name }), 'signed_out');
|
|
238
238
|
try {
|
|
239
|
-
const base = { ...ask, access, accountId: String(c.accountId ?? ''), model: ask.model ?? p.models.strong, base: this.opts.apiBase, fetch: this.opts.fetch };
|
|
239
|
+
const base = { ...ask, access, accountId: String(c.accountId ?? ''), model: ask.model ?? p.models.strong, base: this.opts.apiBase, fetch: this.opts.fetch, originator: ask.originator ?? this.opts.originator };
|
|
240
240
|
return ask.tools ? await respond({ ...base, tools: ask.tools }) : await respond({ ...base, tools: undefined });
|
|
241
241
|
}
|
|
242
242
|
catch (e) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** A host-owned, validated Sign in with ChatGPT token-sharing session. The host completes the official
|
|
2
|
+
* OAuth flow (including ID-token verification), stores it per person, and refreshes it before returning it.
|
|
3
|
+
* This adapter never discovers credentials, starts a login, or substitutes a Codex credential. */
|
|
4
|
+
export type ChatGPTPlanSession = {
|
|
5
|
+
accessToken: string;
|
|
6
|
+
scopes: readonly string[];
|
|
7
|
+
};
|
|
8
|
+
export type ChatGPTPlanAccount = {
|
|
9
|
+
readonly billing: 'subscription';
|
|
10
|
+
access(signal: AbortSignal): Promise<string>;
|
|
11
|
+
};
|
|
12
|
+
/** The selected account, consent or model cannot support this request. Never falls back to API billing. */
|
|
13
|
+
export declare class UnsupportedAccountError extends Error {
|
|
14
|
+
readonly code = "unsupported_account";
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
/** Bind one person's official token-sharing session to this app. Eligibility and identity verification
|
|
18
|
+
* belong to the host's sign-in integration; granted plan-usage scopes are checked on every request. */
|
|
19
|
+
export declare function chatgptPlan(o: {
|
|
20
|
+
session(signal: AbortSignal): Promise<ChatGPTPlanSession>;
|
|
21
|
+
}): ChatGPTPlanAccount;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** The selected account, consent or model cannot support this request. Never falls back to API billing. */
|
|
2
|
+
export class UnsupportedAccountError extends Error {
|
|
3
|
+
code = 'unsupported_account';
|
|
4
|
+
constructor(message) { super(message); this.name = 'UnsupportedAccountError'; }
|
|
5
|
+
}
|
|
6
|
+
/** Bind one person's official token-sharing session to this app. Eligibility and identity verification
|
|
7
|
+
* belong to the host's sign-in integration; granted plan-usage scopes are checked on every request. */
|
|
8
|
+
export function chatgptPlan(o) {
|
|
9
|
+
return {
|
|
10
|
+
billing: 'subscription',
|
|
11
|
+
async access(signal) {
|
|
12
|
+
const session = await o.session(signal);
|
|
13
|
+
if (!session || !Array.isArray(session.scopes) || !session.scopes.includes('chatgpt.tokens.use.direct') ||
|
|
14
|
+
!session.scopes.includes('resource.invoke') || typeof session.accessToken !== 'string' || !session.accessToken.trim()) {
|
|
15
|
+
throw new UnsupportedAccountError('ChatGPT plan usage needs a consented token-sharing session.');
|
|
16
|
+
}
|
|
17
|
+
return session.accessToken;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
package/dist/portable.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export { REST_MS, classify, type Kind } from './limits.ts';
|
|
|
5
5
|
export { ResponseError, isFunctionCall, limitResponse, respond, sseReader, type Ask, type ResponseFunctionCall, type ResponseInputItem, type ResponseOutputItem, type ResponseOutputMessage, type ResponseReasoning, type ResponseResult, type ResponseStreamEvent, type ResponseText, type ResponseTextFormat, type ResponseTool, type ResponseToolChoice } from './responses.ts';
|
|
6
6
|
export { browserStore, memoryStore, recordStore, secureStore, type SecureStoreLike } from './stores.ts';
|
|
7
7
|
export { WORDS, billingWords, callbackPage, clock, failure, say, signInError, type WordKey, type Why } from './words.ts';
|
|
8
|
+
export { chatgptPlan, UnsupportedAccountError, type ChatGPTPlanAccount, type ChatGPTPlanSession } from './chatgpt-plan.ts';
|
package/dist/portable.js
CHANGED
|
@@ -7,3 +7,4 @@ export { REST_MS, classify } from "./limits.js";
|
|
|
7
7
|
export { ResponseError, isFunctionCall, limitResponse, respond, sseReader } from "./responses.js";
|
|
8
8
|
export { browserStore, memoryStore, recordStore, secureStore } from "./stores.js";
|
|
9
9
|
export { WORDS, billingWords, callbackPage, clock, failure, say, signInError } from "./words.js";
|
|
10
|
+
export { chatgptPlan, UnsupportedAccountError } from "./chatgpt-plan.js";
|
package/dist/responses.d.ts
CHANGED
|
@@ -147,7 +147,9 @@ export type ResponseStreamEvent = {
|
|
|
147
147
|
};
|
|
148
148
|
/** Reads a streamed answer (fixtures/conformance/sse.json): `push` each piece as it arrives, `end` for the whole text,
|
|
149
149
|
* `result` for the text with every output item. `onEvent` sees each tool call and output item as it lands.
|
|
150
|
-
*
|
|
150
|
+
* Events split on any blank line (LF, CRLF or bare CR). A data line that is not JSON throws a ResponseError;
|
|
151
|
+
* a stream ending with nothing to show throws too. The text is the streamed deltas; the completed envelope
|
|
152
|
+
* only fills in when no deltas arrived. An error event throws a ResponseError. */
|
|
151
153
|
export declare function sseReader(onText?: (delta: string) => void, onEvent?: (event: ResponseStreamEvent) => void): {
|
|
152
154
|
push(chunk: string): void;
|
|
153
155
|
end(): string;
|
|
@@ -174,6 +176,8 @@ export type Ask = {
|
|
|
174
176
|
/** Each tool call and output item as it lands. */
|
|
175
177
|
onEvent?: (event: ResponseStreamEvent) => void;
|
|
176
178
|
signal?: AbortSignal;
|
|
179
|
+
/** The app's own originator header value. Default: 'byokit'. */
|
|
180
|
+
originator?: string;
|
|
177
181
|
};
|
|
178
182
|
type Access = {
|
|
179
183
|
access: string;
|
package/dist/responses.js
CHANGED
|
@@ -39,7 +39,9 @@ const itemKey = (item, fallback) => typeof item.call_id === 'string' ? item.call
|
|
|
39
39
|
export const isFunctionCall = (item) => isRecord(item) && item.type === 'function_call' && typeof item.name === 'string' && typeof item.arguments === 'string';
|
|
40
40
|
/** Reads a streamed answer (fixtures/conformance/sse.json): `push` each piece as it arrives, `end` for the whole text,
|
|
41
41
|
* `result` for the text with every output item. `onEvent` sees each tool call and output item as it lands.
|
|
42
|
-
*
|
|
42
|
+
* Events split on any blank line (LF, CRLF or bare CR). A data line that is not JSON throws a ResponseError;
|
|
43
|
+
* a stream ending with nothing to show throws too. The text is the streamed deltas; the completed envelope
|
|
44
|
+
* only fills in when no deltas arrived. An error event throws a ResponseError. */
|
|
43
45
|
export function sseReader(onText, onEvent) {
|
|
44
46
|
let buffer = '', text = '', completed;
|
|
45
47
|
let done = false, finished;
|
|
@@ -68,7 +70,7 @@ export function sseReader(onText, onEvent) {
|
|
|
68
70
|
e = JSON.parse(data);
|
|
69
71
|
}
|
|
70
72
|
catch {
|
|
71
|
-
|
|
73
|
+
throw new ResponseError("ChatGPT's answer could not be read.", null);
|
|
72
74
|
}
|
|
73
75
|
if (e.type === 'response.output_text.delta' && typeof e.delta === 'string') {
|
|
74
76
|
text += e.delta;
|
|
@@ -114,8 +116,14 @@ export function sseReader(onText, onEvent) {
|
|
|
114
116
|
}
|
|
115
117
|
};
|
|
116
118
|
const drain = (final) => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
// A trailing CR may be a bare-CR line ending or half of a chunk-split CRLF: hold it until more arrives.
|
|
120
|
+
let tail = '';
|
|
121
|
+
if (!final && buffer.endsWith('\r')) {
|
|
122
|
+
tail = '\r';
|
|
123
|
+
buffer = buffer.slice(0, -1);
|
|
124
|
+
}
|
|
125
|
+
const blocks = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n\n');
|
|
126
|
+
buffer = (final ? '' : blocks.pop()) + tail;
|
|
119
127
|
for (const b of blocks)
|
|
120
128
|
event(b);
|
|
121
129
|
};
|
|
@@ -124,12 +132,11 @@ export function sseReader(onText, onEvent) {
|
|
|
124
132
|
drain(true);
|
|
125
133
|
if (!done)
|
|
126
134
|
throw new ResponseError('ChatGPT stopped before completing its answer.', 'network');
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
whole
|
|
132
|
-
}
|
|
135
|
+
const whole = text !== '' ? text : (completed ?? '');
|
|
136
|
+
if (whole === '' && output.length === 0)
|
|
137
|
+
throw new ResponseError('ChatGPT stopped before completing its answer.', 'network');
|
|
138
|
+
if (text === '' && whole !== '')
|
|
139
|
+
onText?.(whole);
|
|
133
140
|
finished = { text: whole, output };
|
|
134
141
|
}
|
|
135
142
|
return finished;
|
|
@@ -150,7 +157,7 @@ export async function respond(o) {
|
|
|
150
157
|
method: 'POST', signal: o.signal,
|
|
151
158
|
headers: {
|
|
152
159
|
'content-type': 'application/json', accept: 'text/event-stream', authorization: `Bearer ${o.access}`,
|
|
153
|
-
'chatgpt-account-id': o.accountId, 'OpenAI-Beta': 'responses=experimental', originator: 'byokit',
|
|
160
|
+
'chatgpt-account-id': o.accountId, 'OpenAI-Beta': 'responses=experimental', originator: o.originator ?? 'byokit',
|
|
154
161
|
},
|
|
155
162
|
body: JSON.stringify({
|
|
156
163
|
model: o.model, store: false, stream: true, instructions: o.instructions, input,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/accounts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Sign in with the AI plan you already pay for, into your app's own store.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"./testing": {
|
|
33
33
|
"types": "./dist/testing/index.d.ts",
|
|
34
34
|
"default": "./dist/testing/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./chatgpt-plan": {
|
|
37
|
+
"types": "./dist/chatgpt-plan.d.ts",
|
|
38
|
+
"default": "./dist/chatgpt-plan.js"
|
|
35
39
|
}
|
|
36
40
|
},
|
|
37
41
|
"files": [
|