@oxyhq/contracts 0.1.1 → 0.2.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.
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ /**
3
+ * Canonical contract for the FedCM ID-token JWT payload.
4
+ *
5
+ * SINGLE SOURCE OF TRUTH for the decoded claims of the HS256 ID token the auth
6
+ * IdP (`auth.oxy.so`) signs and `POST /fedcm/exchange` consumes. The API decodes
7
+ * the JWT, verifies its signature, then validates the resulting claim object
8
+ * against this schema before trusting any field — a malformed payload (e.g. a
9
+ * forged token whose signature happened to match but whose body is the wrong
10
+ * shape) is rejected at the boundary instead of being cast and used.
11
+ *
12
+ * Validation philosophy — match the existing exchange behaviour exactly:
13
+ * - This schema validates the STRUCTURAL shape of the decoded claims only
14
+ * (types of the fields, not their presence or business-rule validity).
15
+ * - `sub` / `aud` / `nonce` / `iss` / `exp` presence + value checks remain in
16
+ * `fedcm.service.exchangeIdToken`, which returns the specific
17
+ * `missing_required_fields` / `invalid_issuer` / `token_expired` errors. So
18
+ * every field is `.optional()` here: a token missing `nonce` must still reach
19
+ * the `missing_required_fields` branch, not be rejected as a malformed token.
20
+ * - `.passthrough()` preserves any additional claims the IdP may add without a
21
+ * coordinated contract bump.
22
+ *
23
+ * Faithful to the producer:
24
+ * - `packages/auth/server/index.ts` `mintSessionForClient` — builds the
25
+ * assertion with `iss` (central issuer), `sub` (user id), `aud` (RP origin),
26
+ * `exp` / `iat` (numeric epoch seconds), and `nonce` (the server-minted,
27
+ * origin-bound nonce).
28
+ *
29
+ * Platform-agnostic — zod only, no react/react-native/expo. ESM-safe (no
30
+ * `require()`).
31
+ */
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.fedcmTokenPayloadSchema = void 0;
34
+ const zod_1 = require("zod");
35
+ /**
36
+ * Decoded FedCM ID-token claims. Every field is optional because presence is
37
+ * enforced downstream (see module doc); the schema's job is to guarantee that
38
+ * any present claim has the correct primitive type before it is read.
39
+ */
40
+ exports.fedcmTokenPayloadSchema = zod_1.z
41
+ .object({
42
+ iss: zod_1.z.string().optional(),
43
+ sub: zod_1.z.string().optional(),
44
+ aud: zod_1.z.string().optional(),
45
+ exp: zod_1.z.number().optional(),
46
+ iat: zod_1.z.number().optional(),
47
+ nonce: zod_1.z.string().optional(),
48
+ })
49
+ .passthrough();
package/dist/cjs/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * expo, no `require()` in the ESM build.
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.sessionStatusSchema = exports.publicApplicationSchema = exports.applicationTypeSchema = exports.safeParseContract = exports.resolveUserId = exports.deviceSessionsResponseSchema = exports.deviceSessionAccountSchema = exports.currentUserResponseSchema = exports.refreshAllResponseSchema = exports.refreshAllAccountSchema = exports.userProfileUpdateSchema = exports.userResponseSchema = exports.userNameSchema = void 0;
14
+ exports.appUserSignalIngestSchema = exports.appInterestInputSchema = exports.appEndorsementInputSchema = exports.recommendationResponseSchema = exports.recommendationItemSchema = exports.recommendationCountSchema = exports.recommendationRequestSchema = exports.recommendationSignalWeightsSchema = exports.recommendationBoostSchema = exports.recommendationExcludeTypeSchema = exports.fedcmTokenPayloadSchema = exports.sessionStatusSchema = exports.publicApplicationSchema = exports.applicationTypeSchema = exports.safeParseContract = exports.resolveUserId = exports.deviceSessionsResponseSchema = exports.deviceSessionAccountSchema = exports.currentUserResponseSchema = exports.refreshAllResponseSchema = exports.refreshAllAccountSchema = exports.userProfileUpdateSchema = exports.userResponseSchema = exports.userNameSchema = void 0;
15
15
  var userResponse_1 = require("./userResponse");
16
16
  // Schemas
17
17
  Object.defineProperty(exports, "userNameSchema", { enumerable: true, get: function () { return userResponse_1.userNameSchema; } });
@@ -30,3 +30,18 @@ var sessionStatus_1 = require("./sessionStatus");
30
30
  Object.defineProperty(exports, "applicationTypeSchema", { enumerable: true, get: function () { return sessionStatus_1.applicationTypeSchema; } });
31
31
  Object.defineProperty(exports, "publicApplicationSchema", { enumerable: true, get: function () { return sessionStatus_1.publicApplicationSchema; } });
32
32
  Object.defineProperty(exports, "sessionStatusSchema", { enumerable: true, get: function () { return sessionStatus_1.sessionStatusSchema; } });
33
+ var fedcmToken_1 = require("./fedcmToken");
34
+ // Schemas
35
+ Object.defineProperty(exports, "fedcmTokenPayloadSchema", { enumerable: true, get: function () { return fedcmToken_1.fedcmTokenPayloadSchema; } });
36
+ var recommendations_1 = require("./recommendations");
37
+ // Schemas
38
+ Object.defineProperty(exports, "recommendationExcludeTypeSchema", { enumerable: true, get: function () { return recommendations_1.recommendationExcludeTypeSchema; } });
39
+ Object.defineProperty(exports, "recommendationBoostSchema", { enumerable: true, get: function () { return recommendations_1.recommendationBoostSchema; } });
40
+ Object.defineProperty(exports, "recommendationSignalWeightsSchema", { enumerable: true, get: function () { return recommendations_1.recommendationSignalWeightsSchema; } });
41
+ Object.defineProperty(exports, "recommendationRequestSchema", { enumerable: true, get: function () { return recommendations_1.recommendationRequestSchema; } });
42
+ Object.defineProperty(exports, "recommendationCountSchema", { enumerable: true, get: function () { return recommendations_1.recommendationCountSchema; } });
43
+ Object.defineProperty(exports, "recommendationItemSchema", { enumerable: true, get: function () { return recommendations_1.recommendationItemSchema; } });
44
+ Object.defineProperty(exports, "recommendationResponseSchema", { enumerable: true, get: function () { return recommendations_1.recommendationResponseSchema; } });
45
+ Object.defineProperty(exports, "appEndorsementInputSchema", { enumerable: true, get: function () { return recommendations_1.appEndorsementInputSchema; } });
46
+ Object.defineProperty(exports, "appInterestInputSchema", { enumerable: true, get: function () { return recommendations_1.appInterestInputSchema; } });
47
+ Object.defineProperty(exports, "appUserSignalIngestSchema", { enumerable: true, get: function () { return recommendations_1.appUserSignalIngestSchema; } });
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ /**
3
+ * Recommendation-engine API contracts.
4
+ *
5
+ * SINGLE SOURCE OF TRUTH for the wire shape of the reputation-weighted
6
+ * profile-recommendation surface (`POST /profiles/recommendations`) and the
7
+ * cross-app signal-ingest endpoint (`POST /app-signals/ingest`). The API
8
+ * validates its INPUT/OUTPUT against these schemas; consumer SDKs validate the
9
+ * same definitions, so the producer and every consumer cannot drift.
10
+ *
11
+ * Platform-agnostic — zod is the only runtime dependency (no react / react-native
12
+ * / expo, ESM-safe).
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.appUserSignalIngestSchema = exports.appInterestInputSchema = exports.appEndorsementInputSchema = exports.recommendationResponseSchema = exports.recommendationItemSchema = exports.recommendationCountSchema = exports.recommendationRequestSchema = exports.recommendationSignalWeightsSchema = exports.recommendationBoostSchema = exports.recommendationExcludeTypeSchema = void 0;
16
+ const zod_1 = require("zod");
17
+ const userResponse_1 = require("./userResponse");
18
+ /** User-type filters a caller may exclude from the recommendation surface. */
19
+ exports.recommendationExcludeTypeSchema = zod_1.z.enum([
20
+ 'federated',
21
+ 'agent',
22
+ 'automated',
23
+ ]);
24
+ /**
25
+ * A caller-supplied editorial boost. `userIds` are nudged up (or down, for a
26
+ * negative weight) in the ranking; the optional `reason` is for audit/telemetry
27
+ * only and never surfaced to end users. Boost members still pass the eligibility
28
+ * gate — a boost cannot resurrect a private/restricted/ineligible account.
29
+ */
30
+ exports.recommendationBoostSchema = zod_1.z.object({
31
+ userIds: zod_1.z.array(zod_1.z.string().trim().min(1)).min(1).max(200),
32
+ weight: zod_1.z.number().min(-5).max(5),
33
+ reason: zod_1.z.string().trim().max(120).optional(),
34
+ });
35
+ /**
36
+ * Per-request overrides for the scoring signal weights. Every key is optional
37
+ * and clamped server-side to the resolved weight profile's allowed range — a
38
+ * caller can re-weight signals but never escape the profile's bounds.
39
+ */
40
+ exports.recommendationSignalWeightsSchema = zod_1.z
41
+ .object({
42
+ graph: zod_1.z.number().min(0).max(10).optional(),
43
+ completeness: zod_1.z.number().min(0).max(10).optional(),
44
+ verified: zod_1.z.number().min(0).max(10).optional(),
45
+ curation: zod_1.z.number().min(0).max(10).optional(),
46
+ interest: zod_1.z.number().min(0).max(10).optional(),
47
+ appBoost: zod_1.z.number().min(0).max(10).optional(),
48
+ repCandidate: zod_1.z.number().min(0).max(10).optional(),
49
+ })
50
+ .partial();
51
+ /**
52
+ * Request body for `POST /profiles/recommendations`.
53
+ *
54
+ * `clientId` selects the per-app weight profile (the Application `_id`); when
55
+ * omitted the default profile is used. `excludeIds` removes accounts the caller
56
+ * has already seen/handled; `boosts` and `signalWeights` let the caller bias the
57
+ * ranking within server-enforced bounds.
58
+ */
59
+ exports.recommendationRequestSchema = zod_1.z.object({
60
+ clientId: zod_1.z.string().trim().min(1).optional(),
61
+ limit: zod_1.z.number().int().min(1).max(100).optional(),
62
+ offset: zod_1.z.number().int().min(0).optional(),
63
+ excludeTypes: zod_1.z.array(exports.recommendationExcludeTypeSchema).optional(),
64
+ excludeIds: zod_1.z.array(zod_1.z.string().trim().min(1)).max(500).optional(),
65
+ boosts: zod_1.z.array(exports.recommendationBoostSchema).max(50).optional(),
66
+ signalWeights: exports.recommendationSignalWeightsSchema.optional(),
67
+ });
68
+ /** Follower/following counts attached to a recommendation item. */
69
+ exports.recommendationCountSchema = zod_1.z.object({
70
+ followers: zod_1.z.number().int().nonnegative(),
71
+ following: zod_1.z.number().int().nonnegative(),
72
+ });
73
+ /**
74
+ * A single recommended profile.
75
+ *
76
+ * `name` reuses the canonical {@link userNameSchema} so `name.displayName` is the
77
+ * already-resolved server-side value. `score` and `matchedSignals` are present
78
+ * only on the scored (v2) path; `mutualCount` and `_count` are always present.
79
+ */
80
+ exports.recommendationItemSchema = zod_1.z
81
+ .object({
82
+ id: zod_1.z.string(),
83
+ username: zod_1.z.string().optional(),
84
+ name: userResponse_1.userNameSchema,
85
+ avatar: zod_1.z.string().nullable().optional(),
86
+ description: zod_1.z.string().nullable().optional(),
87
+ verified: zod_1.z.boolean().optional(),
88
+ trustTier: zod_1.z.string().optional(),
89
+ mutualCount: zod_1.z.number().int().nonnegative(),
90
+ score: zod_1.z.number().optional(),
91
+ matchedSignals: zod_1.z.array(zod_1.z.string()).optional(),
92
+ isFederated: zod_1.z.boolean().optional(),
93
+ isAgent: zod_1.z.boolean().optional(),
94
+ isAutomated: zod_1.z.boolean().optional(),
95
+ instance: zod_1.z.string().optional(),
96
+ _count: exports.recommendationCountSchema,
97
+ })
98
+ .passthrough();
99
+ /** Wire shape of the recommendation response — an array of items. */
100
+ exports.recommendationResponseSchema = zod_1.z.array(exports.recommendationItemSchema);
101
+ /** One endorsement edge an app reports: `ownerId` endorses `memberId`. */
102
+ exports.appEndorsementInputSchema = zod_1.z.object({
103
+ ownerId: zod_1.z.string().trim().min(1),
104
+ memberId: zod_1.z.string().trim().min(1),
105
+ op: zod_1.z.enum(['add', 'remove']).default('add'),
106
+ sourceId: zod_1.z.string().trim().min(1).optional(),
107
+ });
108
+ /** One interest signal an app reports: how interested `userId` is in a topic. */
109
+ exports.appInterestInputSchema = zod_1.z.object({
110
+ userId: zod_1.z.string().trim().min(1),
111
+ interestScore: zod_1.z.number().min(0).max(1),
112
+ });
113
+ /**
114
+ * Request body for `POST /app-signals/ingest` (service token, `signals:write`).
115
+ *
116
+ * At least one of `endorsements` / `interests` must be non-empty — an ingest
117
+ * with neither is a no-op and rejected so a misconfigured caller is surfaced
118
+ * rather than silently succeeding.
119
+ */
120
+ exports.appUserSignalIngestSchema = zod_1.z
121
+ .object({
122
+ endorsements: zod_1.z.array(exports.appEndorsementInputSchema).max(500).optional(),
123
+ interests: zod_1.z.array(exports.appInterestInputSchema).max(500).optional(),
124
+ })
125
+ .refine((value) => (value.endorsements?.length ?? 0) > 0 || (value.interests?.length ?? 0) > 0, { message: 'At least one of endorsements or interests must be non-empty' });