@oxyhq/contracts 0.1.0 → 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,192 @@
1
+ /**
2
+ * Canonical contract for `GET /auth/session/status/:sessionToken`.
3
+ *
4
+ * SINGLE SOURCE OF TRUTH for the wire shape of the cross-app device-flow
5
+ * session-status payload and the sanitized public application identity it
6
+ * embeds. The API validates its OUTPUT against these schemas; the auth app
7
+ * (consent UI) validates its INPUT against the same schemas. Because there is
8
+ * exactly one definition, the producer and the consumer cannot drift.
9
+ *
10
+ * The class of bug that motivated moving this into `@oxyhq/contracts`: the auth
11
+ * app's LOCAL `sessionStatusSchema` typed `sessionId` as a non-nullable
12
+ * `z.string().optional()`. The producer emits `sessionId: authorizedSessionId ||
13
+ * null`, so a PENDING session (not yet authorized) carries `sessionId: null` —
14
+ * `.optional()` permits `undefined`/missing but REJECTS `null`, so `safeParse`
15
+ * failed, the whole response collapsed to `null`, and the consent screen showed
16
+ * "Unable to identify the requesting application". Pinning the nullability in one
17
+ * shared place makes that drift impossible.
18
+ *
19
+ * Faithful to the producers:
20
+ * - `packages/api/src/utils/serializeApplication.ts` `serializePublicApplication`
21
+ * — the ONLY shape returned to an unauthenticated consent UI. Optional fields
22
+ * (`description`, `icon`, `websiteUrl`, `developerName`) are OMITTED when
23
+ * absent (never serialized as `null`), so they are `.optional()` — NOT
24
+ * `.nullable()`. `type` is the `Application.type` enum.
25
+ * - `packages/api/src/routes/auth.ts` `GET /session/status/:sessionToken` — the
26
+ * inner object of the API's `{ data: ... }` success envelope. The handler
27
+ * ALWAYS emits `status`, `authorized` (`status === 'authorized'`),
28
+ * `sessionToken`, `expiresAt` (ISO string), and `application` (resolved object
29
+ * OR `null`). It ALWAYS emits `sessionId` / `publicKey` / `userId`, each as a
30
+ * string value OR `null` (`authorizedSessionId || null`, `authorizedBy ||
31
+ * null`, `authorizedUserId?.toString() || null`).
32
+ *
33
+ * Platform-agnostic — zod only, no react/react-native/expo. ESM-safe (no
34
+ * `require()`).
35
+ */
36
+ import { z } from 'zod';
37
+ /**
38
+ * Application `type` enum. Mirrors `APPLICATION_TYPES` in
39
+ * `packages/api/src/models/Application.ts` (`first_party` | `third_party` |
40
+ * `internal` | `system`).
41
+ */
42
+ export declare const applicationTypeSchema: z.ZodEnum<["first_party", "third_party", "internal", "system"]>;
43
+ export type ApplicationTypeContract = z.infer<typeof applicationTypeSchema>;
44
+ /**
45
+ * The display-safe public identity of a requesting application, exactly as
46
+ * `serializePublicApplication` emits it. Returned by the API inside
47
+ * `GET /auth/session/status/:sessionToken` (device flow) and
48
+ * `GET /auth/oauth/client/:clientId` (OAuth code flow).
49
+ *
50
+ * Optional fields are `.optional()` (NOT `.nullable()`): the serializer OMITS
51
+ * `description` / `icon` / `websiteUrl` / `developerName` when the underlying
52
+ * value is absent — it never writes `null` for them. `developerName` is only
53
+ * attached for non-official apps when a name could be resolved.
54
+ */
55
+ export declare const publicApplicationSchema: z.ZodObject<{
56
+ id: z.ZodString;
57
+ name: z.ZodString;
58
+ description: z.ZodOptional<z.ZodString>;
59
+ icon: z.ZodOptional<z.ZodString>;
60
+ websiteUrl: z.ZodOptional<z.ZodString>;
61
+ type: z.ZodEnum<["first_party", "third_party", "internal", "system"]>;
62
+ isOfficial: z.ZodBoolean;
63
+ isInternal: z.ZodBoolean;
64
+ scopes: z.ZodArray<z.ZodString, "many">;
65
+ developerName: z.ZodOptional<z.ZodString>;
66
+ }, "strip", z.ZodTypeAny, {
67
+ type: "first_party" | "third_party" | "internal" | "system";
68
+ id: string;
69
+ name: string;
70
+ isOfficial: boolean;
71
+ isInternal: boolean;
72
+ scopes: string[];
73
+ description?: string | undefined;
74
+ icon?: string | undefined;
75
+ websiteUrl?: string | undefined;
76
+ developerName?: string | undefined;
77
+ }, {
78
+ type: "first_party" | "third_party" | "internal" | "system";
79
+ id: string;
80
+ name: string;
81
+ isOfficial: boolean;
82
+ isInternal: boolean;
83
+ scopes: string[];
84
+ description?: string | undefined;
85
+ icon?: string | undefined;
86
+ websiteUrl?: string | undefined;
87
+ developerName?: string | undefined;
88
+ }>;
89
+ export type PublicApplicationResponse = z.infer<typeof publicApplicationSchema>;
90
+ /**
91
+ * The inner object of `GET /auth/session/status/:sessionToken` (inside the API's
92
+ * `{ data: ... }` envelope).
93
+ *
94
+ * `application` is the resolved {@link publicApplicationSchema} identity of the
95
+ * requesting application, or `null` when the bound app was hard-deleted / is no
96
+ * longer `active` (defensive — normally always present).
97
+ *
98
+ * `sessionId` / `publicKey` / `userId` are `.nullable().optional()`: the producer
99
+ * ALWAYS emits the key, with a string for an AUTHORIZED session or `null` for a
100
+ * PENDING one. `.nullable()` accepts the PENDING `null`; `.optional()` is belt-
101
+ * and-braces so a consumer is never broken by a future projection that drops the
102
+ * key. (`.optional()` alone would REJECT the PENDING `null` — that was the bug.)
103
+ *
104
+ * `authorized` / `sessionToken` / `expiresAt` are emitted unconditionally by the
105
+ * current producer and are never `null`, but stay `.optional()` so the contract
106
+ * tolerates leaner shapes from other producers of this same payload without a
107
+ * coordinated bump.
108
+ */
109
+ export declare const sessionStatusSchema: z.ZodObject<{
110
+ status: z.ZodString;
111
+ authorized: z.ZodOptional<z.ZodBoolean>;
112
+ sessionToken: z.ZodOptional<z.ZodString>;
113
+ application: z.ZodOptional<z.ZodNullable<z.ZodObject<{
114
+ id: z.ZodString;
115
+ name: z.ZodString;
116
+ description: z.ZodOptional<z.ZodString>;
117
+ icon: z.ZodOptional<z.ZodString>;
118
+ websiteUrl: z.ZodOptional<z.ZodString>;
119
+ type: z.ZodEnum<["first_party", "third_party", "internal", "system"]>;
120
+ isOfficial: z.ZodBoolean;
121
+ isInternal: z.ZodBoolean;
122
+ scopes: z.ZodArray<z.ZodString, "many">;
123
+ developerName: z.ZodOptional<z.ZodString>;
124
+ }, "strip", z.ZodTypeAny, {
125
+ type: "first_party" | "third_party" | "internal" | "system";
126
+ id: string;
127
+ name: string;
128
+ isOfficial: boolean;
129
+ isInternal: boolean;
130
+ scopes: string[];
131
+ description?: string | undefined;
132
+ icon?: string | undefined;
133
+ websiteUrl?: string | undefined;
134
+ developerName?: string | undefined;
135
+ }, {
136
+ type: "first_party" | "third_party" | "internal" | "system";
137
+ id: string;
138
+ name: string;
139
+ isOfficial: boolean;
140
+ isInternal: boolean;
141
+ scopes: string[];
142
+ description?: string | undefined;
143
+ icon?: string | undefined;
144
+ websiteUrl?: string | undefined;
145
+ developerName?: string | undefined;
146
+ }>>>;
147
+ expiresAt: z.ZodOptional<z.ZodString>;
148
+ sessionId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
149
+ publicKey: z.ZodOptional<z.ZodNullable<z.ZodString>>;
150
+ userId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
151
+ }, "strip", z.ZodTypeAny, {
152
+ status: string;
153
+ publicKey?: string | null | undefined;
154
+ expiresAt?: string | undefined;
155
+ sessionId?: string | null | undefined;
156
+ authorized?: boolean | undefined;
157
+ sessionToken?: string | undefined;
158
+ application?: {
159
+ type: "first_party" | "third_party" | "internal" | "system";
160
+ id: string;
161
+ name: string;
162
+ isOfficial: boolean;
163
+ isInternal: boolean;
164
+ scopes: string[];
165
+ description?: string | undefined;
166
+ icon?: string | undefined;
167
+ websiteUrl?: string | undefined;
168
+ developerName?: string | undefined;
169
+ } | null | undefined;
170
+ userId?: string | null | undefined;
171
+ }, {
172
+ status: string;
173
+ publicKey?: string | null | undefined;
174
+ expiresAt?: string | undefined;
175
+ sessionId?: string | null | undefined;
176
+ authorized?: boolean | undefined;
177
+ sessionToken?: string | undefined;
178
+ application?: {
179
+ type: "first_party" | "third_party" | "internal" | "system";
180
+ id: string;
181
+ name: string;
182
+ isOfficial: boolean;
183
+ isInternal: boolean;
184
+ scopes: string[];
185
+ description?: string | undefined;
186
+ icon?: string | undefined;
187
+ websiteUrl?: string | undefined;
188
+ developerName?: string | undefined;
189
+ } | null | undefined;
190
+ userId?: string | null | undefined;
191
+ }>;
192
+ export type SessionStatusResponse = z.infer<typeof sessionStatusSchema>;