@oxyhq/contracts 0.7.0 → 0.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/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/deviceBoot.js +148 -0
- package/dist/cjs/deviceSession.js +25 -0
- package/dist/cjs/fedcmToken.js +7 -0
- package/dist/cjs/identity.js +16 -1
- package/dist/cjs/index.js +22 -2
- package/dist/cjs/recommendations.js +44 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/deviceBoot.js +145 -0
- package/dist/esm/deviceSession.js +22 -0
- package/dist/esm/fedcmToken.js +7 -0
- package/dist/esm/identity.js +16 -1
- package/dist/esm/index.js +5 -1
- package/dist/esm/recommendations.js +43 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/deviceBoot.d.ts +199 -0
- package/dist/types/deviceSession.d.ts +165 -0
- package/dist/types/fedcmToken.d.ts +21 -0
- package/dist/types/identity.d.ts +62 -7
- package/dist/types/index.d.ts +7 -3
- package/dist/types/recommendations.d.ts +97 -0
- package/dist/types/sessionStatus.d.ts +6 -6
- package/package.json +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-first bootstrap & token contracts (auth centralization, wave 1).
|
|
3
|
+
*
|
|
4
|
+
* SINGLE SOURCE OF TRUTH for the wire shape of the new device-first session
|
|
5
|
+
* bootstrap: the top-level `#oxy_boot=…` fragment the API hands back from
|
|
6
|
+
* `GET /auth/device/bootstrap`, the token bundle a boot code / web-session
|
|
7
|
+
* fast-path exchanges into, the persisted-refresh rotation, the native
|
|
8
|
+
* device-token issuance, the IdP chooser's device-resolve, and the first-party
|
|
9
|
+
* password login result (2FA arm vs. session arm). The API validates its OUTPUT
|
|
10
|
+
* against these schemas; every consumer (`@oxyhq/core`'s device-boot mixin, the
|
|
11
|
+
* SDK cold boot, the IdP chooser) validates its INPUT against the same
|
|
12
|
+
* definitions, so producer and consumers cannot drift.
|
|
13
|
+
*
|
|
14
|
+
* Design anchors (from the auth-centralization plan):
|
|
15
|
+
* - The bootstrap fragment carries NO tokens and NO deviceId — only a
|
|
16
|
+
* `state` echo (CSRF), a `reason`, a short-lived single-use `code`, and an
|
|
17
|
+
* opaque `deviceToken`. Tokens are obtained by exchanging the `code` at
|
|
18
|
+
* `POST /auth/device/exchange` (origin-bound GETDEL burn).
|
|
19
|
+
* - Refresh is ONE rotating, single-use family shared by web and native.
|
|
20
|
+
* - `loginResult` mirrors what `POST /auth/login` returns today
|
|
21
|
+
* (`buildSessionAuthResponse` in the API's `session.controller.ts`): either a
|
|
22
|
+
* 2FA challenge (`{ twoFactorRequired: true, loginToken }`) or a session
|
|
23
|
+
* payload. The session arm matches `SessionAuthResponse` EXACTLY, plus an
|
|
24
|
+
* optional `refreshToken` the new server adds for the persisted-refresh lane.
|
|
25
|
+
*
|
|
26
|
+
* Nested-object response shapes are declared as explicit `interface`s with the
|
|
27
|
+
* runtime schema annotated `z.ZodType<Interface>` — the same rationale as
|
|
28
|
+
* `identity.ts` / `userResponse.ts`: a `z.infer<>` of a nested object schema can
|
|
29
|
+
* degrade to `{}` under a consumer's `moduleResolution: "node"` (node10), so the
|
|
30
|
+
* load-bearing shapes are pinned by literal interfaces. Flat request/response
|
|
31
|
+
* shapes (no nested-object hazard) are inferred via `z.infer<>`.
|
|
32
|
+
*
|
|
33
|
+
* Platform-agnostic — zod only, no react/react-native/expo. ESM-safe (no
|
|
34
|
+
* `require()`).
|
|
35
|
+
*/
|
|
36
|
+
import { z } from 'zod';
|
|
37
|
+
import { userResponseSchema } from './userResponse.js';
|
|
38
|
+
/* -------------------------------------------------------------------------- */
|
|
39
|
+
/* Bootstrap fragment */
|
|
40
|
+
/* -------------------------------------------------------------------------- */
|
|
41
|
+
/**
|
|
42
|
+
* Why the bootstrap hop resolved the way it did.
|
|
43
|
+
* - `session` — the device cookie resolved an active session; a `code` is
|
|
44
|
+
* present to exchange for tokens.
|
|
45
|
+
* - `no_session` — the device is known but has no active session; no `code`.
|
|
46
|
+
* - `new_device` — first contact; the cookie was just planted, no session yet.
|
|
47
|
+
*/
|
|
48
|
+
export const deviceBootReasonSchema = z.enum(['session', 'no_session', 'new_device']);
|
|
49
|
+
/**
|
|
50
|
+
* The `#oxy_boot=<json>` fragment `GET /auth/device/bootstrap` appends to the
|
|
51
|
+
* `return_to` URL. Carries the CSRF `state` echo, the resolution `reason`, an
|
|
52
|
+
* optional single-use exchange `code` (present iff `reason === 'session'`), and
|
|
53
|
+
* the opaque `deviceToken`. NEVER carries tokens or a deviceId.
|
|
54
|
+
*/
|
|
55
|
+
export const deviceBootFragmentSchema = z.object({
|
|
56
|
+
v: z.literal(1),
|
|
57
|
+
state: z.string().min(1).max(256),
|
|
58
|
+
reason: deviceBootReasonSchema,
|
|
59
|
+
code: z.string().min(20).max(128).optional(),
|
|
60
|
+
deviceToken: z.string().min(20).max(512),
|
|
61
|
+
});
|
|
62
|
+
/* -------------------------------------------------------------------------- */
|
|
63
|
+
/* Boot-code exchange */
|
|
64
|
+
/* -------------------------------------------------------------------------- */
|
|
65
|
+
/** Request body for `POST /auth/device/exchange` — the single-use boot code. */
|
|
66
|
+
export const deviceExchangeRequestSchema = z.object({
|
|
67
|
+
code: z.string().min(20).max(128),
|
|
68
|
+
});
|
|
69
|
+
export const authTokenBundleSchema = z.object({
|
|
70
|
+
sessionId: z.string(),
|
|
71
|
+
accessToken: z.string(),
|
|
72
|
+
refreshToken: z.string(),
|
|
73
|
+
expiresAt: z.string(),
|
|
74
|
+
user: userResponseSchema,
|
|
75
|
+
});
|
|
76
|
+
/* -------------------------------------------------------------------------- */
|
|
77
|
+
/* Refresh-token rotation (web + native, one implementation) */
|
|
78
|
+
/* -------------------------------------------------------------------------- */
|
|
79
|
+
/** Request body for `POST /auth/refresh-token` — the current refresh token. */
|
|
80
|
+
export const tokenRefreshRequestSchema = z.object({
|
|
81
|
+
refreshToken: z.string().min(20),
|
|
82
|
+
});
|
|
83
|
+
/**
|
|
84
|
+
* Wire shape of `POST /auth/refresh-token`: the rotated (single-use) family —
|
|
85
|
+
* a new access token, the next refresh token, the new access-token expiry, and
|
|
86
|
+
* the owning session id. `expiresAt` is an ISO string.
|
|
87
|
+
*/
|
|
88
|
+
export const tokenRefreshResponseSchema = z.object({
|
|
89
|
+
accessToken: z.string(),
|
|
90
|
+
refreshToken: z.string(),
|
|
91
|
+
expiresAt: z.string(),
|
|
92
|
+
sessionId: z.string(),
|
|
93
|
+
});
|
|
94
|
+
/* -------------------------------------------------------------------------- */
|
|
95
|
+
/* Native device-token issuance */
|
|
96
|
+
/* -------------------------------------------------------------------------- */
|
|
97
|
+
/**
|
|
98
|
+
* Wire shape of `POST /auth/device/token` — issues (or rotates) the opaque
|
|
99
|
+
* device token for the native channel. The deviceId is taken from the bearer
|
|
100
|
+
* JWT claims server-side; only the token comes back.
|
|
101
|
+
*/
|
|
102
|
+
export const deviceTokenIssueResponseSchema = z.object({
|
|
103
|
+
deviceToken: z.string(),
|
|
104
|
+
});
|
|
105
|
+
const loginTwoFactorRequiredSchema = z.object({
|
|
106
|
+
twoFactorRequired: z.literal(true),
|
|
107
|
+
loginToken: z.string(),
|
|
108
|
+
});
|
|
109
|
+
const loginSessionResultSchema = z.object({
|
|
110
|
+
sessionId: z.string(),
|
|
111
|
+
deviceId: z.string(),
|
|
112
|
+
expiresAt: z.string(),
|
|
113
|
+
accessToken: z.string().optional(),
|
|
114
|
+
refreshToken: z.string().optional(),
|
|
115
|
+
user: z.object({
|
|
116
|
+
id: z.string(),
|
|
117
|
+
username: z.string().optional(),
|
|
118
|
+
avatar: z.string().optional(),
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
export const loginResultSchema = z.union([
|
|
122
|
+
loginTwoFactorRequiredSchema,
|
|
123
|
+
loginSessionResultSchema,
|
|
124
|
+
]);
|
|
125
|
+
/* -------------------------------------------------------------------------- */
|
|
126
|
+
/* IdP chooser device-resolve */
|
|
127
|
+
/* -------------------------------------------------------------------------- */
|
|
128
|
+
/**
|
|
129
|
+
* Request body for `POST /auth/device/resolve` (X-Oxy-Internal, called by the
|
|
130
|
+
* IdP chooser) — the device key the chooser read from the first-party
|
|
131
|
+
* `oxy_device` cookie.
|
|
132
|
+
*/
|
|
133
|
+
export const deviceResolveRequestSchema = z.object({
|
|
134
|
+
deviceKey: z.string().min(20),
|
|
135
|
+
});
|
|
136
|
+
const deviceResolveAccountSchema = z.object({
|
|
137
|
+
user: userResponseSchema,
|
|
138
|
+
sessionId: z.string(),
|
|
139
|
+
accessToken: z.string(),
|
|
140
|
+
expiresAt: z.string(),
|
|
141
|
+
});
|
|
142
|
+
export const deviceResolveResponseSchema = z.object({
|
|
143
|
+
activeAccountId: z.string().nullable(),
|
|
144
|
+
accounts: z.array(deviceResolveAccountSchema),
|
|
145
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const sessionAccountSchema = z.object({
|
|
3
|
+
accountId: z.string(),
|
|
4
|
+
sessionId: z.string(),
|
|
5
|
+
authuser: z.number().int().nonnegative(),
|
|
6
|
+
operatedByUserId: z.string().optional(),
|
|
7
|
+
});
|
|
8
|
+
export const deviceSessionStateSchema = z.object({
|
|
9
|
+
deviceId: z.string(),
|
|
10
|
+
accounts: z.array(sessionAccountSchema),
|
|
11
|
+
activeAccountId: z.string().nullable(),
|
|
12
|
+
revision: z.number().int().nonnegative(),
|
|
13
|
+
updatedAt: z.number(),
|
|
14
|
+
});
|
|
15
|
+
export const activeTokenSchema = z.object({
|
|
16
|
+
accessToken: z.string(),
|
|
17
|
+
expiresAt: z.string(),
|
|
18
|
+
});
|
|
19
|
+
export const deviceSessionSyncSchema = z.object({
|
|
20
|
+
state: deviceSessionStateSchema,
|
|
21
|
+
activeToken: activeTokenSchema.nullable(),
|
|
22
|
+
});
|
package/dist/esm/fedcmToken.js
CHANGED
|
@@ -42,5 +42,12 @@ export const fedcmTokenPayloadSchema = z
|
|
|
42
42
|
exp: z.number().optional(),
|
|
43
43
|
iat: z.number().optional(),
|
|
44
44
|
nonce: z.string().optional(),
|
|
45
|
+
/**
|
|
46
|
+
* An explicit central deviceId minted by the IdP, threaded through so the
|
|
47
|
+
* RP session can inherit a unified device id instead of deriving one from
|
|
48
|
+
* the (userId, RP origin) stableDeviceKey. Optional and additive — omitted
|
|
49
|
+
* tokens fall back to the existing stableDeviceKey/UA-IP derivation.
|
|
50
|
+
*/
|
|
51
|
+
deviceId: z.string().optional(),
|
|
45
52
|
})
|
|
46
53
|
.passthrough();
|
package/dist/esm/identity.js
CHANGED
|
@@ -38,12 +38,27 @@
|
|
|
38
38
|
* `require()`).
|
|
39
39
|
*/
|
|
40
40
|
import { z } from 'zod';
|
|
41
|
-
|
|
41
|
+
// The option schemas are left UN-annotated so they keep their concrete
|
|
42
|
+
// `ZodObject` type — `z.discriminatedUnion` requires object options and an
|
|
43
|
+
// explicit `z.ZodType<>` annotation would erase the shape it discriminates on.
|
|
44
|
+
// `z.object` already infers each option's type exactly (id/type/controller +
|
|
45
|
+
// the key field), so the union is structurally `VerificationMethod`.
|
|
46
|
+
const secp256k1VerificationMethodSchema = z.object({
|
|
42
47
|
id: z.string(),
|
|
43
48
|
type: z.literal('EcdsaSecp256k1VerificationKey2019'),
|
|
44
49
|
controller: z.string(),
|
|
45
50
|
publicKeyHex: z.string(),
|
|
46
51
|
});
|
|
52
|
+
const multikeyVerificationMethodSchema = z.object({
|
|
53
|
+
id: z.string(),
|
|
54
|
+
type: z.literal('Multikey'),
|
|
55
|
+
controller: z.string(),
|
|
56
|
+
publicKeyMultibase: z.string(),
|
|
57
|
+
});
|
|
58
|
+
export const verificationMethodSchema = z.discriminatedUnion('type', [
|
|
59
|
+
secp256k1VerificationMethodSchema,
|
|
60
|
+
multikeyVerificationMethodSchema,
|
|
61
|
+
]);
|
|
47
62
|
export const didServiceSchema = z.object({
|
|
48
63
|
id: z.string(),
|
|
49
64
|
type: z.string(),
|
package/dist/esm/index.js
CHANGED
|
@@ -22,7 +22,7 @@ export {
|
|
|
22
22
|
fedcmTokenPayloadSchema, } from './fedcmToken.js';
|
|
23
23
|
export {
|
|
24
24
|
// Schemas
|
|
25
|
-
recommendationExcludeTypeSchema, recommendationBoostSchema, recommendationSignalWeightsSchema, recommendationRequestSchema, recommendationCountSchema, recommendationItemSchema, recommendationResponseSchema, appEndorsementInputSchema, appInterestInputSchema, appUserSignalIngestSchema, } from './recommendations.js';
|
|
25
|
+
recommendationExcludeTypeSchema, recommendationBoostSchema, recommendationSignalWeightsSchema, recommendationRequestSchema, recommendationCountSchema, recommendationItemSchema, recommendationResponseSchema, appEndorsementInputSchema, appInterestInputSchema, appUserSignalIngestSchema, appAffinityEventTypeSchema, appAffinityEventSchema, appAffinityEventsIngestSchema, } from './recommendations.js';
|
|
26
26
|
export {
|
|
27
27
|
// Schemas
|
|
28
28
|
verificationMethodSchema, didServiceSchema, didDocumentSchema, signedRecordEnvelopeSchema, verifiedDomainSchema, domainVerificationRequestSchema, domainVerificationInstructionsSchema, authMethodEntrySchema, authMethodsResponseSchema, exportAttestationSchema, exportBundleSchema, } from './identity.js';
|
|
@@ -40,3 +40,7 @@ credentialRecordSchema, verifiableCredentialResponseSchema, credentialIssueResul
|
|
|
40
40
|
export {
|
|
41
41
|
// Schemas
|
|
42
42
|
linkPreviewSchema, linkPreviewBatchRequestSchema, linkPreviewBatchResponseSchema, linkPreviewResponseSchema, } from './links.js';
|
|
43
|
+
export { sessionAccountSchema, deviceSessionStateSchema, activeTokenSchema, deviceSessionSyncSchema, } from './deviceSession.js';
|
|
44
|
+
export {
|
|
45
|
+
// Schemas
|
|
46
|
+
deviceBootReasonSchema, deviceBootFragmentSchema, deviceExchangeRequestSchema, authTokenBundleSchema, tokenRefreshRequestSchema, tokenRefreshResponseSchema, deviceTokenIssueResponseSchema, loginResultSchema, deviceResolveRequestSchema, deviceResolveResponseSchema, } from './deviceBoot.js';
|
|
@@ -43,6 +43,7 @@ export const recommendationSignalWeightsSchema = z
|
|
|
43
43
|
interest: z.number().min(0).max(10).optional(),
|
|
44
44
|
appBoost: z.number().min(0).max(10).optional(),
|
|
45
45
|
repCandidate: z.number().min(0).max(10).optional(),
|
|
46
|
+
affinity: z.number().min(0).max(10).optional(),
|
|
46
47
|
})
|
|
47
48
|
.partial();
|
|
48
49
|
/**
|
|
@@ -120,3 +121,45 @@ export const appUserSignalIngestSchema = z
|
|
|
120
121
|
interests: z.array(appInterestInputSchema).max(500).optional(),
|
|
121
122
|
})
|
|
122
123
|
.refine((value) => (value.endorsements?.length ?? 0) > 0 || (value.interests?.length ?? 0) > 0, { message: 'At least one of endorsements or interests must be non-empty' });
|
|
124
|
+
/**
|
|
125
|
+
* The directed interaction types a consuming app may report between two users.
|
|
126
|
+
* Each type carries a server-side default weight (see the API's
|
|
127
|
+
* `AFFINITY_EVENT_WEIGHTS`); a caller may override the applied weight per event.
|
|
128
|
+
*/
|
|
129
|
+
export const appAffinityEventTypeSchema = z.enum([
|
|
130
|
+
'like',
|
|
131
|
+
'reply',
|
|
132
|
+
'boost',
|
|
133
|
+
'follow',
|
|
134
|
+
'mention',
|
|
135
|
+
'profile_view',
|
|
136
|
+
'quote',
|
|
137
|
+
'repost',
|
|
138
|
+
]);
|
|
139
|
+
/**
|
|
140
|
+
* One directed interaction event: `fromUserId` interacted with `toUserId`
|
|
141
|
+
* (`type`) at `occurredAt`. The Oxy affinity-graph folds these into a per-app,
|
|
142
|
+
* time-decayed directed affinity edge (`fromUserId → toUserId`).
|
|
143
|
+
*
|
|
144
|
+
* - `weight` (optional) overrides the per-type default weight for this event.
|
|
145
|
+
* - `occurredAt` (optional, ISO) is the event time; absent means "now" at ingest.
|
|
146
|
+
* - `eventId` (optional) makes an event idempotent — a repeated `eventId` for the
|
|
147
|
+
* same application is folded at most once (bounded dedup window).
|
|
148
|
+
*/
|
|
149
|
+
export const appAffinityEventSchema = z.object({
|
|
150
|
+
fromUserId: z.string().trim().min(1),
|
|
151
|
+
toUserId: z.string().trim().min(1),
|
|
152
|
+
type: appAffinityEventTypeSchema,
|
|
153
|
+
weight: z.number().min(0).max(100).optional(),
|
|
154
|
+
occurredAt: z.string().datetime().optional(),
|
|
155
|
+
eventId: z.string().trim().min(1).max(200).optional(),
|
|
156
|
+
});
|
|
157
|
+
/**
|
|
158
|
+
* Request body for `POST /app-signals/events` (service token, `signals:write`).
|
|
159
|
+
*
|
|
160
|
+
* A non-empty batch (1..1000) of directed interaction events for the requesting
|
|
161
|
+
* application. Self-edges (`fromUserId === toUserId`) are dropped server-side.
|
|
162
|
+
*/
|
|
163
|
+
export const appAffinityEventsIngestSchema = z.object({
|
|
164
|
+
events: z.array(appAffinityEventSchema).min(1).max(1000),
|
|
165
|
+
});
|