@oxyhq/contracts 0.25.0 → 0.27.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/NOTICE +10 -9
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/accountGraph.js +4 -3
- package/dist/cjs/browserHub.js +215 -0
- package/dist/cjs/deviceDirectory.js +189 -0
- package/dist/cjs/index.js +172 -2
- package/dist/cjs/inference/attribution.js +101 -0
- package/dist/cjs/inference/catalogue.js +482 -0
- package/dist/cjs/inference/errors.js +195 -0
- package/dist/cjs/inference/identifiers.js +189 -0
- package/dist/cjs/inference/money.js +145 -0
- package/dist/cjs/inference/priceVersion.js +110 -0
- package/dist/cjs/inference/providerConnection.js +142 -0
- package/dist/cjs/inference/request.js +288 -0
- package/dist/cjs/inference/routingPolicy.js +213 -0
- package/dist/cjs/inference/streamEvents.js +219 -0
- package/dist/cjs/inference/usage.js +291 -0
- package/dist/cjs/inference/version.js +57 -0
- package/dist/cjs/oauth.js +66 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/accountGraph.js +4 -3
- package/dist/esm/browserHub.js +212 -0
- package/dist/esm/deviceDirectory.js +186 -0
- package/dist/esm/index.js +48 -0
- package/dist/esm/inference/attribution.js +98 -0
- package/dist/esm/inference/catalogue.js +479 -0
- package/dist/esm/inference/errors.js +192 -0
- package/dist/esm/inference/identifiers.js +186 -0
- package/dist/esm/inference/money.js +142 -0
- package/dist/esm/inference/priceVersion.js +107 -0
- package/dist/esm/inference/providerConnection.js +139 -0
- package/dist/esm/inference/request.js +285 -0
- package/dist/esm/inference/routingPolicy.js +210 -0
- package/dist/esm/inference/streamEvents.js +216 -0
- package/dist/esm/inference/usage.js +288 -0
- package/dist/esm/inference/version.js +54 -0
- package/dist/esm/oauth.js +63 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/accountGraph.d.ts +6 -5
- package/dist/types/browserHub.d.ts +856 -0
- package/dist/types/deviceDirectory.d.ts +1317 -0
- package/dist/types/deviceSession.d.ts +46 -46
- package/dist/types/index.d.ts +29 -0
- package/dist/types/inference/attribution.d.ts +171 -0
- package/dist/types/inference/catalogue.d.ts +1612 -0
- package/dist/types/inference/errors.d.ts +193 -0
- package/dist/types/inference/identifiers.d.ts +149 -0
- package/dist/types/inference/money.d.ts +142 -0
- package/dist/types/inference/priceVersion.d.ts +182 -0
- package/dist/types/inference/providerConnection.d.ts +297 -0
- package/dist/types/inference/request.d.ts +2364 -0
- package/dist/types/inference/routingPolicy.d.ts +426 -0
- package/dist/types/inference/streamEvents.d.ts +906 -0
- package/dist/types/inference/usage.d.ts +1133 -0
- package/dist/types/inference/version.d.ts +54 -0
- package/dist/types/oauth.d.ts +86 -0
- package/dist/types/sessionStatus.d.ts +8 -8
- package/dist/types/userResponse.d.ts +8 -8
- package/package.json +1 -1
package/dist/esm/accountGraph.js
CHANGED
|
@@ -14,9 +14,10 @@ import { z } from 'zod';
|
|
|
14
14
|
* `db/schema/users.ts` mirrors to keep the `users_kind_check` CHECK honest.
|
|
15
15
|
*
|
|
16
16
|
* Deriving the union from the array instead would cost nothing here and be paid
|
|
17
|
-
* by consumers: `kind` travels into `@oxyhq/services`
|
|
18
|
-
* `
|
|
19
|
-
* expensive to check than a
|
|
17
|
+
* by consumers: `kind` travels into `@oxyhq/services` on every device-directory
|
|
18
|
+
* context (`deviceContextSchema.kind` → `DeviceContext` → the switcher rows),
|
|
19
|
+
* and an indexed-access type is materially more expensive to check there than a
|
|
20
|
+
* literal union.
|
|
20
21
|
*/
|
|
21
22
|
export const ACCOUNT_KINDS = [
|
|
22
23
|
'personal',
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { deviceDirectorySchema } from './deviceDirectory.js';
|
|
3
|
+
/**
|
|
4
|
+
* The browser DeviceSession hub at `auth.oxy.so` (issue #937, Phase 5, ADR 0003).
|
|
5
|
+
*
|
|
6
|
+
* Two wire surfaces live in this file and they are deliberately different
|
|
7
|
+
* shapes, because they answer to different callers:
|
|
8
|
+
*
|
|
9
|
+
* - The `browserHub*` schemas below the first divider are the API's
|
|
10
|
+
* (`api.oxy.so/session/browser-hub/*`), spoken ONLY by the IdP's own
|
|
11
|
+
* server/edge layer. They carry the raw handle and a bearer, so nothing that
|
|
12
|
+
* speaks them may run in a browser.
|
|
13
|
+
* - The `hub*` schemas below the second divider are the EDGE's
|
|
14
|
+
* (`auth.oxy.so/hub/*`), spoken by the IdP SPA. They carry neither: the
|
|
15
|
+
* handle stays in an `HttpOnly` cookie the script cannot read, and the
|
|
16
|
+
* device-wide bearer stays at the edge.
|
|
17
|
+
*
|
|
18
|
+
* That split is the whole point of the phase. A single schema covering both
|
|
19
|
+
* would let a refactor move a credential across the boundary without any type
|
|
20
|
+
* changing shape.
|
|
21
|
+
*
|
|
22
|
+
* ## What is NOT reopened here
|
|
23
|
+
*
|
|
24
|
+
* Relying-party origins remain zero-cookie: they keep `{deviceId,
|
|
25
|
+
* deviceSecret}` + `POST /session/device/token` and set no cookie of any kind.
|
|
26
|
+
* `auth.oxy.so` alone holds a handle, first-party only. There is no
|
|
27
|
+
* refresh-token family and no bootstrap hop.
|
|
28
|
+
*/
|
|
29
|
+
/* -------------------------------------------------------------------------- */
|
|
30
|
+
/* The cookie */
|
|
31
|
+
/* -------------------------------------------------------------------------- */
|
|
32
|
+
/**
|
|
33
|
+
* The one cookie name, `__Host-` prefixed.
|
|
34
|
+
*
|
|
35
|
+
* The prefix is not decoration: a browser refuses to store a `__Host-` cookie
|
|
36
|
+
* that carries a `Domain` attribute or a `Path` other than `/`, or that arrives
|
|
37
|
+
* without `Secure`. So the name itself is what makes "bound to `auth.oxy.so`
|
|
38
|
+
* alone, readable by no other `oxy.so` host" enforced by the client rather than
|
|
39
|
+
* merely intended by the server — including against a compromised sibling
|
|
40
|
+
* subdomain, which is the specific attack `Domain=.oxy.so` would have opened.
|
|
41
|
+
*/
|
|
42
|
+
export const BROWSER_HUB_COOKIE_NAME = '__Host-oxy-device';
|
|
43
|
+
/**
|
|
44
|
+
* The exact attribute set the edge sets, in the order it writes them.
|
|
45
|
+
*
|
|
46
|
+
* Held as data rather than as a template string so a test can assert the set
|
|
47
|
+
* rather than a rendered line, and so removing one attribute is a diff to a
|
|
48
|
+
* named constant instead of an edit inside a string literal.
|
|
49
|
+
*
|
|
50
|
+
* `Max-Age` is deliberately NOT in this list — see
|
|
51
|
+
* {@link BROWSER_HUB_HANDLE_TTL_MS}, which the edge appends. Everything here is
|
|
52
|
+
* a SECURITY attribute and none of them is ever conditional.
|
|
53
|
+
*/
|
|
54
|
+
export const BROWSER_HUB_COOKIE_ATTRIBUTES = ['Secure', 'HttpOnly', 'SameSite=Lax', 'Path=/'];
|
|
55
|
+
/**
|
|
56
|
+
* How long a hub handle lives, server-side and in the cookie alike.
|
|
57
|
+
*
|
|
58
|
+
* Thirty days. The cookie's `Max-Age` is derived from this same constant so the
|
|
59
|
+
* two cannot disagree: a cookie outliving its credential produces a browser that
|
|
60
|
+
* believes it is signed in and is refused on every call, and a credential
|
|
61
|
+
* outliving its cookie leaves an un-addressable row alive on the server.
|
|
62
|
+
*
|
|
63
|
+
* A hub handle is NOT a session cookie (one with no `Max-Age`, discarded when
|
|
64
|
+
* the browser closes). It cannot be: the thing it identifies is the browser
|
|
65
|
+
* PROFILE's device session, and a device session that evaporates when the user
|
|
66
|
+
* quits their browser would send them back to a QR scan every morning — the
|
|
67
|
+
* exact failure ADR 0003 exists to remove.
|
|
68
|
+
*/
|
|
69
|
+
export const BROWSER_HUB_HANDLE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
70
|
+
/* -------------------------------------------------------------------------- */
|
|
71
|
+
/* api.oxy.so/session/browser-hub/* — spoken by the edge, never a browser */
|
|
72
|
+
/* -------------------------------------------------------------------------- */
|
|
73
|
+
/**
|
|
74
|
+
* The raw handle, as it travels between the edge and the API.
|
|
75
|
+
*
|
|
76
|
+
* An opaque base64url random value and NOTHING else — no token, no user id, no
|
|
77
|
+
* device id, no account id, no serialized state. The server stores only
|
|
78
|
+
* `sha256(handle)`, so a dump of the column cannot address a browser.
|
|
79
|
+
*
|
|
80
|
+
* The minimum length is a floor against an empty or truncated value reaching a
|
|
81
|
+
* hash comparison, not a statement about the entropy: that is fixed by the
|
|
82
|
+
* issuer (`BROWSER_HUB_HANDLE_BYTES` in the API), and a handle is the only
|
|
83
|
+
* credential in this flow, so it is 256 bits.
|
|
84
|
+
*/
|
|
85
|
+
export const browserHubHandleSchema = z.string().min(32);
|
|
86
|
+
/** Request body of every handle-presenting API endpoint. */
|
|
87
|
+
export const browserHubHandleRequestSchema = z.object({
|
|
88
|
+
handle: browserHubHandleSchema,
|
|
89
|
+
});
|
|
90
|
+
/**
|
|
91
|
+
* Response of `POST /session/browser-hub/establish` and `.../rotate`.
|
|
92
|
+
*
|
|
93
|
+
* The raw handle is returned exactly ONCE, to the edge, which puts it straight
|
|
94
|
+
* into the cookie and keeps no copy. It is never logged, never re-readable, and
|
|
95
|
+
* never reaches the browser's script context.
|
|
96
|
+
*/
|
|
97
|
+
export const browserHubHandleResponseSchema = z.object({
|
|
98
|
+
handle: browserHubHandleSchema,
|
|
99
|
+
expiresAt: z.string(),
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* Response of `POST /session/browser-hub/resolve` — the browser's device
|
|
103
|
+
* session, resolved from the handle alone.
|
|
104
|
+
*
|
|
105
|
+
* Carries a bearer because the edge needs one to run the authorize lane on the
|
|
106
|
+
* browser's behalf. It is the ordinary short-lived access token of the device's
|
|
107
|
+
* active context, and it stops at the edge.
|
|
108
|
+
*/
|
|
109
|
+
export const browserHubResolveResponseSchema = z.object({
|
|
110
|
+
accessToken: z.string().min(1),
|
|
111
|
+
expiresAt: z.string(),
|
|
112
|
+
directory: deviceDirectorySchema,
|
|
113
|
+
});
|
|
114
|
+
/**
|
|
115
|
+
* Why a handle did not resolve. A CLOSED set, and deliberately coarse.
|
|
116
|
+
*
|
|
117
|
+
* - `invalid_handle` — unknown, expired, or revoked. One code for all
|
|
118
|
+
* three: distinguishing them would tell a caller
|
|
119
|
+
* holding a guessed value whether it ever existed.
|
|
120
|
+
* - `no_active_session` — the handle is good and the device has nothing live
|
|
121
|
+
* to mint for. The credential is NOT revoked; the
|
|
122
|
+
* browser re-authenticates and keeps its cookie.
|
|
123
|
+
*/
|
|
124
|
+
export const browserHubErrorSchema = z.enum(['invalid_handle', 'no_active_session']);
|
|
125
|
+
/** Response of `POST /session/browser-hub/revoke`. Idempotent by construction. */
|
|
126
|
+
export const browserHubRevokeResponseSchema = z.object({
|
|
127
|
+
revoked: z.boolean(),
|
|
128
|
+
});
|
|
129
|
+
/* -------------------------------------------------------------------------- */
|
|
130
|
+
/* auth.oxy.so/hub/* — spoken by the IdP SPA */
|
|
131
|
+
/* -------------------------------------------------------------------------- */
|
|
132
|
+
/**
|
|
133
|
+
* What the SPA is allowed to know about the hub session.
|
|
134
|
+
*
|
|
135
|
+
* `signed_out` covers "no cookie", "cookie present but the handle no longer
|
|
136
|
+
* resolves" and "resolved, but the device has nothing live" — from the script's
|
|
137
|
+
* side those are one state, and collapsing them here is what stops a UI from
|
|
138
|
+
* branching on a distinction it must not act on.
|
|
139
|
+
*
|
|
140
|
+
* `active` carries the DIRECTORY and no credential. A switcher renders from it;
|
|
141
|
+
* nothing in it can be spent against the API. The bearer that produced it never
|
|
142
|
+
* left the edge.
|
|
143
|
+
*/
|
|
144
|
+
export const hubSessionSchema = z.discriminatedUnion('status', [
|
|
145
|
+
z.object({ status: z.literal('signed_out') }),
|
|
146
|
+
z.object({ status: z.literal('active'), directory: deviceDirectorySchema }),
|
|
147
|
+
]);
|
|
148
|
+
/** Request body of `POST /hub/claim` — the Commons approval lane's handoff. */
|
|
149
|
+
export const hubClaimRequestSchema = z.object({
|
|
150
|
+
/**
|
|
151
|
+
* The secret `sessionToken` of an approved `AuthSession`, held only by the
|
|
152
|
+
* page that created it. The edge spends it server-side: the access token and
|
|
153
|
+
* device secret the claim yields are consumed to establish the hub and are
|
|
154
|
+
* then discarded, so neither is ever returned to the script.
|
|
155
|
+
*/
|
|
156
|
+
sessionToken: z.string().min(1),
|
|
157
|
+
});
|
|
158
|
+
/** Request body of `POST /hub/activate` — pick the globally active context. */
|
|
159
|
+
export const hubActivateRequestSchema = z.object({
|
|
160
|
+
contextId: z.string().min(1),
|
|
161
|
+
});
|
|
162
|
+
/**
|
|
163
|
+
* Request body of `POST /hub/authorize` — a later official origin joining.
|
|
164
|
+
*
|
|
165
|
+
* There is no `prompt` field, and that is not an omission. `'none'` is absent
|
|
166
|
+
* from `buildOAuthAuthorizeUrl`'s union in `@oxyhq/core` precisely so a silent
|
|
167
|
+
* loop cannot be rebuilt in one line, and this endpoint would be the second
|
|
168
|
+
* place to rebuild it. A caller that needs a login or consent prompt gets one
|
|
169
|
+
* by not passing `approve`.
|
|
170
|
+
*/
|
|
171
|
+
export const hubAuthorizeRequestSchema = z.object({
|
|
172
|
+
clientId: z.string().min(1),
|
|
173
|
+
redirectUri: z.string().url(),
|
|
174
|
+
state: z.string().optional(),
|
|
175
|
+
codeChallenge: z.string().min(1),
|
|
176
|
+
/** S256 only. `plain` is refused before the request leaves the edge. */
|
|
177
|
+
codeChallengeMethod: z.literal('S256'),
|
|
178
|
+
scope: z.string().optional(),
|
|
179
|
+
/**
|
|
180
|
+
* The user's explicit answer on the consent screen.
|
|
181
|
+
*
|
|
182
|
+
* Absent or `false` means "tell me whether consent is needed"; the edge then
|
|
183
|
+
* returns `consent_required` and mints nothing. Only `true` may mint, and only
|
|
184
|
+
* the consent screen's own button sends it — which is why the decision of
|
|
185
|
+
* WHETHER consent is required is re-read from the server on both passes rather
|
|
186
|
+
* than remembered from the first.
|
|
187
|
+
*/
|
|
188
|
+
approve: z.boolean().optional(),
|
|
189
|
+
});
|
|
190
|
+
/**
|
|
191
|
+
* Result of `POST /hub/authorize`.
|
|
192
|
+
*
|
|
193
|
+
* `signed_out` is the honest answer for a browser with no hub session: the SPA
|
|
194
|
+
* runs the ordinary Commons-first authentication and tries again. It is never a
|
|
195
|
+
* redirect the edge performs on the browser's behalf — no automatic chain
|
|
196
|
+
* across Oxy origins, and nothing here can be hidden inside an iframe.
|
|
197
|
+
*/
|
|
198
|
+
export const hubAuthorizeResultSchema = z.discriminatedUnion('status', [
|
|
199
|
+
z.object({ status: z.literal('signed_out') }),
|
|
200
|
+
z.object({
|
|
201
|
+
status: z.literal('consent_required'),
|
|
202
|
+
reason: z.enum(['new', 'scope_changed']),
|
|
203
|
+
userConsentScopes: z.array(z.string()).optional(),
|
|
204
|
+
}),
|
|
205
|
+
z.object({
|
|
206
|
+
status: z.literal('code'),
|
|
207
|
+
code: z.string().min(1),
|
|
208
|
+
state: z.string().nullable(),
|
|
209
|
+
redirectUri: z.string(),
|
|
210
|
+
expiresIn: z.number().int().positive(),
|
|
211
|
+
}),
|
|
212
|
+
]);
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { accountKindSchema } from './accountGraph.js';
|
|
3
|
+
import { activeTokenSchema, deviceSessionStateSchema } from './deviceSession.js';
|
|
4
|
+
import { userNameSchema } from './userResponse.js';
|
|
5
|
+
/**
|
|
6
|
+
* The canonical device directory — the ONE server-authoritative read model an
|
|
7
|
+
* account switcher renders (issue #937, ADR 0002).
|
|
8
|
+
*
|
|
9
|
+
* It replaces the client-side union of `DeviceSessionState.accounts[]` with a
|
|
10
|
+
* separately fetched `AccountNode[]` graph. That union cannot be correct on a
|
|
11
|
+
* device holding more than one person: the client only ever holds ONE caller's
|
|
12
|
+
* account graph, so it cannot enumerate what the OTHER principals may act as.
|
|
13
|
+
* Switchability is an authorization question, so the server answers it.
|
|
14
|
+
*
|
|
15
|
+
* The directory is deterministic and revision-bound: two reads at the same
|
|
16
|
+
* `revision` describe the same device state, and `revision` only advances on a
|
|
17
|
+
* real mutation (an idempotent activation advances nothing — ADR 0002).
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* How a principal reaches an account.
|
|
21
|
+
*
|
|
22
|
+
* Mirrors `AccountRelationship` in `@oxyhq/core`'s account graph rather than
|
|
23
|
+
* inventing a second vocabulary for the same fact:
|
|
24
|
+
* - `self` — the principal's own personal account (`principal.userId === accountId`)
|
|
25
|
+
* - `owner` — the principal owns this account
|
|
26
|
+
* - `member` — the principal holds a membership granting `account:act_as`
|
|
27
|
+
*/
|
|
28
|
+
export const deviceContextRelationshipSchema = z.enum(['self', 'owner', 'member']);
|
|
29
|
+
/**
|
|
30
|
+
* Sanitized display metadata for a principal or an account context.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately NOT `userResponseSchema`: a switcher needs a handle, a name, an
|
|
33
|
+
* avatar and the accent the row is drawn in, and the directory is read by every
|
|
34
|
+
* app on the device — so it carries the minimum that renders a row and nothing
|
|
35
|
+
* that would make it a general profile feed. `name.displayName` stays OPTIONAL;
|
|
36
|
+
* consumers fall back to the handle (`getNormalizedUserHandle`), never to a
|
|
37
|
+
* synthesized name.
|
|
38
|
+
*
|
|
39
|
+
* `color` is here and `email` is not, and the line between them is what the
|
|
40
|
+
* field is FOR rather than how sensitive it looks. An accent is a property of
|
|
41
|
+
* drawing the row — without it every non-active row falls back to the ambient
|
|
42
|
+
* theme accent and a device holding two people renders them identically
|
|
43
|
+
* (issue #961). An address is somebody's contact detail, and the `@handle` the
|
|
44
|
+
* secondary line already shows fills the same slot, so putting one in a payload
|
|
45
|
+
* every installed app reads would widen the profile for no rendering gain.
|
|
46
|
+
*/
|
|
47
|
+
export const deviceDirectoryProfileSchema = z.object({
|
|
48
|
+
id: z.string().min(1),
|
|
49
|
+
username: z.string(),
|
|
50
|
+
name: userNameSchema.optional(),
|
|
51
|
+
avatar: z.string().nullable().optional(),
|
|
52
|
+
/**
|
|
53
|
+
* Named Bloom color preset (e.g. `"blue"`), or null when the account has
|
|
54
|
+
* none. Same shape as `userResponseSchema.color` — one spelling of one fact,
|
|
55
|
+
* so a consumer that themes a row from either source reads the same values.
|
|
56
|
+
*/
|
|
57
|
+
color: z.string().nullable().optional(),
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* One `principal acting as account` pair — the globally switchable unit.
|
|
61
|
+
*
|
|
62
|
+
* `id` is the identifier `POST /session/device/activate` takes. It names the
|
|
63
|
+
* PAIR, not the account: the same `accountId` legitimately appears under two
|
|
64
|
+
* principals on a shared device, and those are different sessions, permissions,
|
|
65
|
+
* audit actors and revocation paths.
|
|
66
|
+
*
|
|
67
|
+
* `onDevice` is `false` for a context the principal may act as but has never
|
|
68
|
+
* activated here — it exists as a row so it has a stable id to activate, and
|
|
69
|
+
* its delegated session is minted on first activation rather than eagerly for
|
|
70
|
+
* every organization the principal belongs to.
|
|
71
|
+
*
|
|
72
|
+
* `available` is `principalLive && (personal || live account:act_as)`, and the
|
|
73
|
+
* principal clause is not decoration: a principal whose own personal session
|
|
74
|
+
* has died makes EVERY context of theirs unavailable, including delegated ones
|
|
75
|
+
* whose sessions are perfectly alive. Activation verifies the principal's
|
|
76
|
+
* personal session (ADR 0002 step 3), so a client that models availability as
|
|
77
|
+
* "delegated context has a live session ⇒ activatable" will be refused by the
|
|
78
|
+
* server and the context healed away.
|
|
79
|
+
*
|
|
80
|
+
* A managed account whose membership was revoked is returned with
|
|
81
|
+
* `available: false` rather than silently omitted, so the UI can explain a row
|
|
82
|
+
* disappearing instead of just dropping it.
|
|
83
|
+
*
|
|
84
|
+
* (An earlier version of this comment described `available` as the
|
|
85
|
+
* `account:act_as` verdict alone. It was written before the server existed and
|
|
86
|
+
* was weaker than the code that shipped — the kind of wrong statement nothing
|
|
87
|
+
* recomputes, so it is spelled out here in full.)
|
|
88
|
+
*/
|
|
89
|
+
export const deviceAccountContextSchema = z.object({
|
|
90
|
+
id: z.string().min(1),
|
|
91
|
+
accountId: z.string().min(1),
|
|
92
|
+
kind: accountKindSchema,
|
|
93
|
+
relationship: deviceContextRelationshipSchema,
|
|
94
|
+
account: deviceDirectoryProfileSchema,
|
|
95
|
+
onDevice: z.boolean(),
|
|
96
|
+
available: z.boolean(),
|
|
97
|
+
active: z.boolean(),
|
|
98
|
+
lastUsedAt: z.number().nullable(),
|
|
99
|
+
});
|
|
100
|
+
/**
|
|
101
|
+
* A human who authenticated onto this device.
|
|
102
|
+
*
|
|
103
|
+
* Never an organization, project, channel or bot — those are subjects a
|
|
104
|
+
* principal acts as, and they appear only in `contexts`. `authuser` is the
|
|
105
|
+
* Google-style signed-in-human slot and belongs HERE, not to an account: adding
|
|
106
|
+
* an organization must never consume one.
|
|
107
|
+
*/
|
|
108
|
+
export const devicePrincipalSchema = z.object({
|
|
109
|
+
id: z.string().min(1),
|
|
110
|
+
userId: z.string().min(1),
|
|
111
|
+
authuser: z.number().int().nonnegative(),
|
|
112
|
+
user: deviceDirectoryProfileSchema,
|
|
113
|
+
contexts: z.array(deviceAccountContextSchema),
|
|
114
|
+
});
|
|
115
|
+
/**
|
|
116
|
+
* Response of `GET /session/device/directory`.
|
|
117
|
+
*
|
|
118
|
+
* `activeContextId` is the authority for what every official app in
|
|
119
|
+
* `sessionMode: 'account'` renders. It is null when the device has no active
|
|
120
|
+
* context — a real state (every context removed, or an active context healed
|
|
121
|
+
* away), not an error.
|
|
122
|
+
*/
|
|
123
|
+
export const deviceDirectorySchema = z.object({
|
|
124
|
+
deviceId: z.string().min(1),
|
|
125
|
+
revision: z.number().int().nonnegative(),
|
|
126
|
+
activeContextId: z.string().nullable(),
|
|
127
|
+
principals: z.array(devicePrincipalSchema),
|
|
128
|
+
updatedAt: z.number(),
|
|
129
|
+
});
|
|
130
|
+
/**
|
|
131
|
+
* Request body of `POST /session/device/activate`.
|
|
132
|
+
*
|
|
133
|
+
* `contextId`, never `accountId`: an account id cannot name a context on a
|
|
134
|
+
* device where two people can both reach the same organization, and resolving
|
|
135
|
+
* that ambiguity server-side would mean guessing inside an authorization path.
|
|
136
|
+
*/
|
|
137
|
+
export const deviceActivateRequestSchema = z.object({
|
|
138
|
+
contextId: z.string().min(1),
|
|
139
|
+
});
|
|
140
|
+
/**
|
|
141
|
+
* Response of `POST /session/device/activate`.
|
|
142
|
+
*
|
|
143
|
+
* Carries the post-transition directory AND the bearer for the newly active
|
|
144
|
+
* context, because the client's ordering invariant is
|
|
145
|
+
* `commit token → reset caches → publish snapshot → notify` (ADR 0002). Handing
|
|
146
|
+
* the directory back without the token would force a second round trip in the
|
|
147
|
+
* middle of that sequence, which is exactly where a component would render the
|
|
148
|
+
* new subject while still holding the previous subject's bearer.
|
|
149
|
+
*
|
|
150
|
+
* `activeToken` is null when the activation legitimately produced no bearer for
|
|
151
|
+
* THIS caller — an identity-pinned client, or a caller whose application is not
|
|
152
|
+
* entitled to a token for the new context. It is never an error signal.
|
|
153
|
+
*/
|
|
154
|
+
export const deviceActivateResponseSchema = z.object({
|
|
155
|
+
directory: deviceDirectorySchema,
|
|
156
|
+
activeToken: activeTokenSchema.nullable(),
|
|
157
|
+
});
|
|
158
|
+
/**
|
|
159
|
+
* Response of the CONTEXT-aware removals — `POST /session/device/signout` with
|
|
160
|
+
* `{ contextId }` (one `principal → account` pair) or `{ principalId }` (one
|
|
161
|
+
* person and every context they reach, and nobody else's).
|
|
162
|
+
*
|
|
163
|
+
* It carries BOTH halves because a removal elects a replacement active context,
|
|
164
|
+
* so both the directory and the flat compatibility projection move in the same
|
|
165
|
+
* transition — and a client that learned only one of them would render one
|
|
166
|
+
* half of a device that no longer exists.
|
|
167
|
+
*
|
|
168
|
+
* This is deliberately NOT {@link deviceSessionSyncSchema} and must never be
|
|
169
|
+
* merged into it. A zod object strips unknown keys, so `{directory, state,
|
|
170
|
+
* activeToken}` parses cleanly as `{state, activeToken}` — silently dropping the
|
|
171
|
+
* directory. One schema covering both shapes would therefore make "the server
|
|
172
|
+
* stopped sending the directory" and "this endpoint never sends one"
|
|
173
|
+
* indistinguishable at the parse, on the exact path that decides which identity
|
|
174
|
+
* the app is running as. Two schemas fail closed instead: `directory` is
|
|
175
|
+
* required here, so a directory-less payload is refused outright.
|
|
176
|
+
*
|
|
177
|
+
* `activeToken` is null on the same terms as everywhere else — an identity-
|
|
178
|
+
* pinned caller, or one not entitled to a bearer for the newly-elected context —
|
|
179
|
+
* and null is also the honest answer when the removal left the device with no
|
|
180
|
+
* active context at all.
|
|
181
|
+
*/
|
|
182
|
+
export const deviceDirectorySyncSchema = z.object({
|
|
183
|
+
directory: deviceDirectorySchema,
|
|
184
|
+
state: deviceSessionStateSchema,
|
|
185
|
+
activeToken: activeTokenSchema.nullable(),
|
|
186
|
+
});
|
package/dist/esm/index.js
CHANGED
|
@@ -70,6 +70,9 @@ export {
|
|
|
70
70
|
// Schemas
|
|
71
71
|
linkPreviewSchema, linkPreviewBatchRequestSchema, linkPreviewBatchResponseSchema, linkPreviewResponseSchema, } from './links.js';
|
|
72
72
|
export { sessionAccountSchema, deviceSessionStateSchema, activeTokenSchema, deviceSessionSyncSchema, deviceTokenMintRequestSchema, deviceTokenMintResponseSchema, deviceBackgroundCredentialResponseSchema, deviceBackgroundTokenRequestSchema, deviceBackgroundTokenResponseSchema, SESSION_ACCOUNTS_CHANGED_EVENT, sessionAccountsChangedReasonSchema, sessionAccountsChangedEventSchema, } from './deviceSession.js';
|
|
73
|
+
export { deviceContextRelationshipSchema, deviceDirectoryProfileSchema, deviceAccountContextSchema, devicePrincipalSchema, deviceDirectorySchema, deviceActivateRequestSchema, deviceActivateResponseSchema, deviceDirectorySyncSchema, } from './deviceDirectory.js';
|
|
74
|
+
export { oauthConsentDecisionSchema, oauthAuthorizeCodeResponseSchema, } from './oauth.js';
|
|
75
|
+
export { BROWSER_HUB_COOKIE_NAME, BROWSER_HUB_COOKIE_ATTRIBUTES, BROWSER_HUB_HANDLE_TTL_MS, browserHubHandleSchema, browserHubHandleRequestSchema, browserHubHandleResponseSchema, browserHubResolveResponseSchema, browserHubErrorSchema, browserHubRevokeResponseSchema, hubSessionSchema, hubClaimRequestSchema, hubActivateRequestSchema, hubAuthorizeRequestSchema, hubAuthorizeResultSchema, } from './browserHub.js';
|
|
73
76
|
export {
|
|
74
77
|
// Schemas
|
|
75
78
|
loginResultSchema, } from './deviceBoot.js';
|
|
@@ -99,3 +102,48 @@ devicePairingStatusSchema, deviceTransferInitRequestSchema, deviceTransferInitRe
|
|
|
99
102
|
export {
|
|
100
103
|
// Schemas — transparency log (checkpoints + inclusion proofs)
|
|
101
104
|
transparencyCheckpointSignatureSchema, transparencyAnchorSchema, transparencyCheckpointSchema, transparencyInclusionProofSchema, transparencyCheckpointListSchema, } from './transparency.js';
|
|
105
|
+
/* -------------------------------------------------------------------------- */
|
|
106
|
+
/* Inference (Oxy↔data-plane) — issue #972 */
|
|
107
|
+
/* -------------------------------------------------------------------------- */
|
|
108
|
+
export {
|
|
109
|
+
// The version of the contract SET; per-shape versions live in the data.
|
|
110
|
+
INFERENCE_CONTRACT_VERSION, } from './inference/version.js';
|
|
111
|
+
export {
|
|
112
|
+
// Principal identifiers. `oxyAccountIdSchema` and `delegatedUserIdSchema`
|
|
113
|
+
// are branded apart so a delegated end user can never become the payer.
|
|
114
|
+
oxyAccountIdSchema, delegatedUserIdSchema, oxyApplicationIdSchema, oxyCredentialIdSchema, requestIdSchema, generationIdSchema, idempotencyKeySchema, inferenceEnvironmentSchema,
|
|
115
|
+
// Wire primitives
|
|
116
|
+
inferenceTimestampSchema, inferenceDateSchema, inferenceHttpsUrlSchema,
|
|
117
|
+
// Catalogue references
|
|
118
|
+
publisherSlugSchema, modelSlugSchema, modelIdSchema, modelRevisionLabelSchema, modelReferenceSchema, routingProfileSlugSchema, inferenceProviderSlugSchema, deploymentIdSchema, inferenceRegionSchema, RESERVED_ALIA_PUBLISHER, } from './inference/identifiers.js';
|
|
119
|
+
export {
|
|
120
|
+
// Exact money and metered units — never floats, units never money.
|
|
121
|
+
currencyCodeSchema, INFERENCE_MONEY_SCALE, exactDecimalSchema, moneySchema, USAGE_UNITS, usageUnitSchema, USAGE_SOURCES, usageSourceSchema, usageQuantitySchema, unitPriceSchema, } from './inference/money.js';
|
|
122
|
+
export {
|
|
123
|
+
// Canonical attribution: who pays, which app, which credential, which user.
|
|
124
|
+
INFERENCE_SCOPES, inferenceScopeSchema, billingPrincipalSchema, authenticatedPrincipalSchema, inferenceAttributionSchema, } from './inference/attribution.js';
|
|
125
|
+
export {
|
|
126
|
+
// Closed error vocabulary + retryability + a leak-proof provider passthrough.
|
|
127
|
+
INFERENCE_ERROR_CODES, NON_RETRYABLE_INFERENCE_ERROR_CODES, inferenceErrorCodeSchema, upstreamErrorCategorySchema, safeErrorTextSchema, providerErrorPassthroughSchema, inferenceErrorSchema, } from './inference/errors.js';
|
|
128
|
+
export {
|
|
129
|
+
// Price versions and the snapshot a settled receipt keeps.
|
|
130
|
+
priceVersionStatusSchema, priceVersionSchema, priceSnapshotSchema, } from './inference/priceVersion.js';
|
|
131
|
+
export {
|
|
132
|
+
// The six distinct catalogue objects + the customer-safe projection.
|
|
133
|
+
inferenceModalitySchema, modelCapabilitiesSchema, modelLicenseSchema, modelProvenanceSchema, inferenceDataPolicySchema, availabilityScopeSchema, commercialPermissionSchema, modelDeprecationSchema, modelEvaluationResultSchema, modelSafetyMetadataSchema, modelPublisherSchema, catalogueModelSchema, modelRevisionSchema, inferenceProviderSchema, modelDeploymentSchema, routingProfileCandidateSchema, routingProfileSchema, cataloguePublisherSummarySchema, catalogueServingProviderSummarySchema, modelCatalogueEntrySchema, } from './inference/catalogue.js';
|
|
134
|
+
export {
|
|
135
|
+
// Routing policy: every control, plus the refinement that rejects a policy
|
|
136
|
+
// no route could ever satisfy.
|
|
137
|
+
routingTargetSchema, routingPolicyScopeSchema, routingFallbackPolicySchema, routingPolicySchema, routingPolicyReferenceSchema, } from './inference/routingPolicy.js';
|
|
138
|
+
export {
|
|
139
|
+
// The normalized Oxy→data-plane request envelope.
|
|
140
|
+
inferenceContentSourceSchema, inferenceContentPartSchema, inferenceToolCallSchema, inferenceMessageRoleSchema, inferenceMessageSchema, inferenceInputSchema, samplingParametersSchema, toolDefinitionSchema, toolChoiceSchema, responseFormatSchema, clientRequestMetadataSchema, inferenceRequestSchema, } from './inference/request.js';
|
|
141
|
+
export {
|
|
142
|
+
// Normalized SSE events.
|
|
143
|
+
inferenceStreamStartEventSchema, inferenceStreamDeltaEventSchema, inferenceStreamToolCallEventSchema, inferenceStreamUsageEventSchema, inferenceRouteSwitchDetailSchema, inferenceRouteSwitchReasonSchema, inferenceStreamRouteSwitchEventSchema, inferenceStreamErrorEventSchema, inferenceFinishReasonSchema, inferenceStreamDoneEventSchema, inferenceStreamEventSchema, } from './inference/streamEvents.js';
|
|
144
|
+
export {
|
|
145
|
+
// Reserve → settle → refund.
|
|
146
|
+
usageReservationRequestSchema, usageReservationStatusSchema, usageReservationSchema, inferenceRequestOutcomeSchema, normalizedUsageReportSchema, usageReceiptSchema, usageRefundSubjectSchema, usageRefundReasonSchema, usageRefundSchema, } from './inference/usage.js';
|
|
147
|
+
export {
|
|
148
|
+
// BYOK connection metadata that structurally cannot carry a secret.
|
|
149
|
+
providerConnectionScopeSchema, providerSecretReferenceSchema, providerConnectionValidationSchema, providerConnectionStatusSchema, providerConnectionSchema, } from './inference/providerConnection.js';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical attribution for an inference request.
|
|
3
|
+
*
|
|
4
|
+
* Every accepted request resolves to an Oxy account, an Oxy application, the
|
|
5
|
+
* Oxy credential that authenticated it, an optional delegated end user, and the
|
|
6
|
+
* ids that correlate it across the edge, the data plane and the ledger. The
|
|
7
|
+
* data plane may store these as immutable references; it never owns or mutates
|
|
8
|
+
* them, and it never mints a customer identity of its own.
|
|
9
|
+
*
|
|
10
|
+
* The rule this file encodes structurally, rather than restating in prose:
|
|
11
|
+
* **the delegated `userId` can never be the billing identity.** Two independent
|
|
12
|
+
* mechanisms enforce it, one at compile time and one at parse time, because a
|
|
13
|
+
* delegated identity being charged for somebody else's workload is the kind of
|
|
14
|
+
* mistake that produces a correct-looking invoice for the wrong customer:
|
|
15
|
+
*
|
|
16
|
+
* 1. `accountId` and `userId` carry DIFFERENT brands, so neither is assignable
|
|
17
|
+
* to the other in any consumer without a cast.
|
|
18
|
+
* 2. {@link billingPrincipalSchema} is `.strict()` and holds exactly one field,
|
|
19
|
+
* so a payload that smuggles `userId` into the billing block is rejected at
|
|
20
|
+
* the parse rather than stripped and forgotten.
|
|
21
|
+
*
|
|
22
|
+
* These shapes are EMBEDDED — they ride inside a request envelope, a receipt or
|
|
23
|
+
* a ledger record and inherit its `schemaVersion`. Versioning them separately
|
|
24
|
+
* would let one message claim two versions.
|
|
25
|
+
*
|
|
26
|
+
* Decided in: docs/adr/0007-canonical-request-attribution.md.
|
|
27
|
+
*/
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
import { delegatedUserIdSchema, generationIdSchema, inferenceEnvironmentSchema, oxyAccountIdSchema, oxyApplicationIdSchema, oxyCredentialIdSchema, requestIdSchema, } from './identifiers.js';
|
|
30
|
+
/**
|
|
31
|
+
* The inference capability scopes the data plane needs to know about.
|
|
32
|
+
*
|
|
33
|
+
* A credential may carry many other Oxy scopes; only these cross the boundary,
|
|
34
|
+
* because the data plane's authorization questions are exactly "may this caller
|
|
35
|
+
* invoke", "may it read the catalogue", "may it read usage", "may it read or
|
|
36
|
+
* write routing", "may it read or write provider connections". Everything else
|
|
37
|
+
* is the control plane's business and is not the data plane's to hold.
|
|
38
|
+
*/
|
|
39
|
+
export const INFERENCE_SCOPES = [
|
|
40
|
+
'inference:invoke',
|
|
41
|
+
'inference:models:read',
|
|
42
|
+
'inference:usage:read',
|
|
43
|
+
'inference:routing:read',
|
|
44
|
+
'inference:routing:write',
|
|
45
|
+
'inference:providers:read',
|
|
46
|
+
'inference:providers:write',
|
|
47
|
+
];
|
|
48
|
+
export const inferenceScopeSchema = z.enum(INFERENCE_SCOPES);
|
|
49
|
+
/**
|
|
50
|
+
* The financially responsible principal, and the ONLY identity a charge may be
|
|
51
|
+
* booked against.
|
|
52
|
+
*
|
|
53
|
+
* It is its own type — not a field on a larger principal object — precisely so
|
|
54
|
+
* that a function taking "who pays" cannot be handed a user, a session, a
|
|
55
|
+
* device or an application. It cannot be constructed from a delegated user id:
|
|
56
|
+
* the brands differ, and this object accepts no other key.
|
|
57
|
+
*/
|
|
58
|
+
export const billingPrincipalSchema = z
|
|
59
|
+
.object({
|
|
60
|
+
accountId: oxyAccountIdSchema,
|
|
61
|
+
})
|
|
62
|
+
.strict();
|
|
63
|
+
/**
|
|
64
|
+
* Who authenticated, as resolved by the Oxy edge before a request is forwarded.
|
|
65
|
+
*
|
|
66
|
+
* Mirrors what a verified Oxy service token carries (`appId`, `credentialId`,
|
|
67
|
+
* `ownerAccountId`, `environment`, effective scopes) so that the two
|
|
68
|
+
* authentication paths — a machine API key and a first-party service token —
|
|
69
|
+
* produce one shape downstream. The data plane authorizes against this
|
|
70
|
+
* envelope; it does not re-derive access from its own database, because it has
|
|
71
|
+
* no account graph to re-derive it from.
|
|
72
|
+
*/
|
|
73
|
+
export const authenticatedPrincipalSchema = z.object({
|
|
74
|
+
billing: billingPrincipalSchema,
|
|
75
|
+
applicationId: oxyApplicationIdSchema,
|
|
76
|
+
credentialId: oxyCredentialIdSchema,
|
|
77
|
+
environment: inferenceEnvironmentSchema,
|
|
78
|
+
inferenceScopes: z.array(inferenceScopeSchema),
|
|
79
|
+
});
|
|
80
|
+
/**
|
|
81
|
+
* The attribution block carried by every request, receipt and ledger record.
|
|
82
|
+
*
|
|
83
|
+
* `userId` is the OPTIONAL delegated end user — Alia's `X-Oxy-User-Id`. It is
|
|
84
|
+
* attribution only: it never changes which account is charged, never grants
|
|
85
|
+
* access, and lives outside {@link billingPrincipalSchema} so that no code path
|
|
86
|
+
* can read it as the payer.
|
|
87
|
+
*
|
|
88
|
+
* `requestId` is generated by the data plane and always present; `generationId`
|
|
89
|
+
* is present
|
|
90
|
+
* once a generation exists, which is why it is optional on a request and
|
|
91
|
+
* expected on a receipt.
|
|
92
|
+
*/
|
|
93
|
+
export const inferenceAttributionSchema = z.object({
|
|
94
|
+
principal: authenticatedPrincipalSchema,
|
|
95
|
+
userId: delegatedUserIdSchema.optional(),
|
|
96
|
+
requestId: requestIdSchema,
|
|
97
|
+
generationId: generationIdSchema.optional(),
|
|
98
|
+
});
|