@syra.fm/sdk 0.9.0 → 0.11.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/lib/commonjs/client.js +247 -7
- package/lib/commonjs/client.js.map +1 -1
- package/lib/commonjs/index.js +36 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/live/components/CreateRoomSheet.js +5 -5
- package/lib/commonjs/live/components/CreateRoomSheet.js.map +1 -1
- package/lib/commonjs/live/components/RecordingsPanel.js +9 -9
- package/lib/commonjs/live/components/RecordingsPanel.js.map +1 -1
- package/lib/commonjs/live/validation.js +44 -15
- package/lib/commonjs/live/validation.js.map +1 -1
- package/lib/commonjs/schema.js +80 -9
- package/lib/commonjs/schema.js.map +1 -1
- package/lib/module/client.js +248 -8
- package/lib/module/client.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/live/components/CreateRoomSheet.js +5 -5
- package/lib/module/live/components/CreateRoomSheet.js.map +1 -1
- package/lib/module/live/components/RecordingsPanel.js +9 -9
- package/lib/module/live/components/RecordingsPanel.js.map +1 -1
- package/lib/module/live/validation.js +44 -15
- package/lib/module/live/validation.js.map +1 -1
- package/lib/module/schema.js +79 -8
- package/lib/module/schema.js.map +1 -1
- package/lib/typescript/commonjs/client.d.ts +171 -2
- package/lib/typescript/commonjs/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +3 -3
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/live/components/RoomCard.d.ts +1 -1
- package/lib/typescript/commonjs/live/components/RoomCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/live/validation.d.ts +17 -20
- package/lib/typescript/commonjs/live/validation.d.ts.map +1 -1
- package/lib/typescript/commonjs/schema.d.ts +97 -7
- package/lib/typescript/commonjs/schema.d.ts.map +1 -1
- package/lib/typescript/module/client.d.ts +171 -2
- package/lib/typescript/module/client.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -3
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/live/components/RoomCard.d.ts +1 -1
- package/lib/typescript/module/live/components/RoomCard.d.ts.map +1 -1
- package/lib/typescript/module/live/validation.d.ts +17 -20
- package/lib/typescript/module/live/validation.d.ts.map +1 -1
- package/lib/typescript/module/schema.d.ts +97 -7
- package/lib/typescript/module/schema.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/client.ts +430 -10
- package/src/index.ts +17 -0
- package/src/live/components/CreateRoomSheet.tsx +5 -5
- package/src/live/components/RecordingsPanel.tsx +9 -9
- package/src/live/components/RoomCard.tsx +1 -1
- package/src/live/validation.ts +41 -15
- package/src/schema.ts +90 -7
- package/src/client.test.ts +0 -632
package/src/live/validation.ts
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Accept a document whose identity arrives as `_id` and present it as `id`.
|
|
5
|
+
*
|
|
6
|
+
* The PostgreSQL migration made `id` REQUIRED on the four schemas below, which is
|
|
7
|
+
* correct against the ported backend — `_id` left the wire contract there. But the
|
|
8
|
+
* frontend deploys on its own path (`deploy-frontends.yml`, on `packages/frontend`
|
|
9
|
+
* and `packages/shared-types`) while the backend deploy is gated, so a merge to
|
|
10
|
+
* `main` ships the client half ALONE. It did: `GET /rooms` still answers `_id`,
|
|
11
|
+
* every room failed `ZRoom`, and `validateRooms` DROPS what it cannot parse — so
|
|
12
|
+
* the Live screen went empty in production with only a `console.warn` to say so.
|
|
13
|
+
*
|
|
14
|
+
* Normalising here rather than teaching call sites to read `room.id ?? room._id`
|
|
15
|
+
* is the architecture this repo already states: parse once at the API boundary and
|
|
16
|
+
* return typed data. One place, no UI knows, and it is correct against BOTH
|
|
17
|
+
* backends — so the cutover needs no coordinated client flip, which is the failure
|
|
18
|
+
* this comment exists because of.
|
|
19
|
+
*
|
|
20
|
+
* It can be deleted once no deployed Syra backend serves `_id`. Nothing breaks if
|
|
21
|
+
* it outlives that: a payload carrying only `id` passes through untouched.
|
|
22
|
+
*/
|
|
23
|
+
function withMongoIdFallback<T extends z.ZodTypeAny>(schema: T) {
|
|
24
|
+
return z.preprocess((value) => {
|
|
25
|
+
if (value === null || typeof value !== 'object') return value;
|
|
26
|
+
const doc = value as Record<string, unknown>;
|
|
27
|
+
if (typeof doc['id'] === 'string' || typeof doc['_id'] !== 'string') return value;
|
|
28
|
+
return { ...doc, id: doc['_id'] };
|
|
29
|
+
}, schema);
|
|
30
|
+
}
|
|
31
|
+
|
|
3
32
|
// --- Podcast queue ---
|
|
4
33
|
|
|
5
34
|
/**
|
|
@@ -17,9 +46,8 @@ export type PodcastQueueItem = z.infer<typeof ZPodcastQueueItem>;
|
|
|
17
46
|
|
|
18
47
|
// --- Room (replaces Space) ---
|
|
19
48
|
|
|
20
|
-
export const ZRoom = z.object({
|
|
21
|
-
|
|
22
|
-
id: z.string().optional(),
|
|
49
|
+
export const ZRoom = withMongoIdFallback(z.object({
|
|
50
|
+
id: z.string(),
|
|
23
51
|
title: z.string(),
|
|
24
52
|
description: z.string().nullish(),
|
|
25
53
|
|
|
@@ -78,7 +106,7 @@ export const ZRoom = z.object({
|
|
|
78
106
|
recordingEgressId: z.string().nullish(),
|
|
79
107
|
|
|
80
108
|
createdAt: z.string(),
|
|
81
|
-
}).passthrough();
|
|
109
|
+
}).passthrough());
|
|
82
110
|
|
|
83
111
|
export type Room = z.infer<typeof ZRoom>;
|
|
84
112
|
|
|
@@ -117,9 +145,8 @@ export const ZHouseVisibility = z.object({
|
|
|
117
145
|
|
|
118
146
|
export type HouseVisibility = z.infer<typeof ZHouseVisibility>;
|
|
119
147
|
|
|
120
|
-
export const ZHouse = z.object({
|
|
121
|
-
|
|
122
|
-
id: z.string().optional(),
|
|
148
|
+
export const ZHouse = withMongoIdFallback(z.object({
|
|
149
|
+
id: z.string(),
|
|
123
150
|
name: z.string(),
|
|
124
151
|
description: z.string().optional(),
|
|
125
152
|
avatar: z.string().optional().nullable(),
|
|
@@ -132,7 +159,7 @@ export const ZHouse = z.object({
|
|
|
132
159
|
visibility: ZHouseVisibility.default({ discovery: 'listed', rooms: 'anyone', join: 'invite' }),
|
|
133
160
|
tags: z.array(z.string()).optional(),
|
|
134
161
|
createdAt: z.string(),
|
|
135
|
-
}).passthrough();
|
|
162
|
+
}).passthrough());
|
|
136
163
|
|
|
137
164
|
export type House = z.infer<typeof ZHouse>;
|
|
138
165
|
|
|
@@ -167,9 +194,8 @@ export const ZSeriesEpisode = z.object({
|
|
|
167
194
|
|
|
168
195
|
export type SeriesEpisode = z.infer<typeof ZSeriesEpisode>;
|
|
169
196
|
|
|
170
|
-
export const ZSeries = z.object({
|
|
171
|
-
|
|
172
|
-
id: z.string().optional(),
|
|
197
|
+
export const ZSeries = withMongoIdFallback(z.object({
|
|
198
|
+
id: z.string(),
|
|
173
199
|
title: z.string(),
|
|
174
200
|
description: z.string().optional(),
|
|
175
201
|
coverImage: z.string().optional().nullable(),
|
|
@@ -181,14 +207,14 @@ export const ZSeries = z.object({
|
|
|
181
207
|
nextEpisodeNumber: z.number().default(1),
|
|
182
208
|
isActive: z.boolean().default(true),
|
|
183
209
|
createdAt: z.string(),
|
|
184
|
-
}).passthrough();
|
|
210
|
+
}).passthrough());
|
|
185
211
|
|
|
186
212
|
export type Series = z.infer<typeof ZSeries>;
|
|
187
213
|
|
|
188
214
|
// --- Recording ---
|
|
189
215
|
|
|
190
|
-
export const ZRecording = z.object({
|
|
191
|
-
|
|
216
|
+
export const ZRecording = withMongoIdFallback(z.object({
|
|
217
|
+
id: z.string(),
|
|
192
218
|
roomId: z.string(),
|
|
193
219
|
roomTitle: z.string(),
|
|
194
220
|
host: z.string(),
|
|
@@ -203,7 +229,7 @@ export const ZRecording = z.object({
|
|
|
203
229
|
participantIds: z.array(z.string()).default([]),
|
|
204
230
|
expiresAt: z.string(),
|
|
205
231
|
createdAt: z.string(),
|
|
206
|
-
}).passthrough();
|
|
232
|
+
}).passthrough());
|
|
207
233
|
|
|
208
234
|
export type Recording = z.infer<typeof ZRecording>;
|
|
209
235
|
|
package/src/schema.ts
CHANGED
|
@@ -59,6 +59,9 @@ export type TrackSummary = z.infer<typeof trackSummarySchema>;
|
|
|
59
59
|
* variant `url` is `/api/images/:id`); `imageSourceUrl` keeps the original
|
|
60
60
|
* external artwork URL as an absolute fallback when re-hosting has not run yet.
|
|
61
61
|
*/
|
|
62
|
+
export const podcastVisibilitySchema = z.enum(['private', 'unlisted', 'public']);
|
|
63
|
+
export type PodcastVisibility = z.infer<typeof podcastVisibilitySchema>;
|
|
64
|
+
|
|
62
65
|
export const podcastSummarySchema = z.object({
|
|
63
66
|
id: z.string(),
|
|
64
67
|
title: z.string(),
|
|
@@ -67,6 +70,15 @@ export const podcastSummarySchema = z.object({
|
|
|
67
70
|
image: z.string().optional(),
|
|
68
71
|
imageSizes: coverArtSizesSchema.optional(),
|
|
69
72
|
imageSourceUrl: z.string().optional(),
|
|
73
|
+
/**
|
|
74
|
+
* Who may see this show. OPTIONAL here although the API always sends it: a
|
|
75
|
+
* published SDK is installed against whatever version of the API a consumer
|
|
76
|
+
* happens to be pointing at, and requiring a field the previous release did
|
|
77
|
+
* not send would make this SDK reject every response from it.
|
|
78
|
+
*/
|
|
79
|
+
visibility: podcastVisibilitySchema.optional(),
|
|
80
|
+
/** Disclosure: the show's content was machine-generated. Optional, same reason. */
|
|
81
|
+
aiGenerated: z.boolean().optional(),
|
|
70
82
|
});
|
|
71
83
|
export type PodcastSummary = z.infer<typeof podcastSummarySchema>;
|
|
72
84
|
|
|
@@ -75,12 +87,28 @@ export type PodcastSummary = z.infer<typeof podcastSummarySchema>;
|
|
|
75
87
|
* (`GET /api/podcasts/:id/episodes`, `GET /api/episodes/:id`) — just enough to
|
|
76
88
|
* list an episode and stream its audio.
|
|
77
89
|
*
|
|
78
|
-
* `enclosureUrl` is
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
90
|
+
* ## `enclosureUrl` is OPTIONAL, and that is a bug fix rather than a loosening
|
|
91
|
+
*
|
|
92
|
+
* It used to be required, on the reasoning that "an episode with no enclosure is
|
|
93
|
+
* unplayable, so a row missing it is malformed". That reasoning holds only for
|
|
94
|
+
* RSS-mirrored episodes. A SYRA-HOSTED episode — everything created through
|
|
95
|
+
* `createPodcast`/`uploadEpisode`/the ingest ticket — has no enclosure at all:
|
|
96
|
+
* its audio lives at `audioSource.url`, a path on the Syra API. So the required
|
|
97
|
+
* field made this SDK silently DROP every Syra-hosted episode from
|
|
98
|
+
* `getPodcastEpisodes` and throw on one from `getEpisode` — the entire
|
|
99
|
+
* first-party catalogue, invisible, with no error to notice.
|
|
100
|
+
*
|
|
101
|
+
* The two are alternatives, not a required field and an optional one:
|
|
102
|
+
*
|
|
103
|
+
* enclosureUrl an ABSOLUTE external URL (RSS mirror)
|
|
104
|
+
* audioSource.url a PATH on the Syra API (Syra-hosted)
|
|
105
|
+
*
|
|
106
|
+
* `SyraClient.episodeAudioUrl` is the one place that resolves either into
|
|
107
|
+
* something playable, so no consumer has to know which kind it holds.
|
|
108
|
+
*
|
|
109
|
+
* `enclosureType` / `enclosureLength` describe the external file (MIME type and
|
|
110
|
+
* byte length); `duration` is the runtime in seconds and `pubDate` the ISO
|
|
111
|
+
* publish timestamp.
|
|
84
112
|
*
|
|
85
113
|
* Artwork mirrors the podcast SHOW: `image` is the re-hosted Syra image id
|
|
86
114
|
* (resolved via `/api/images/:id`); `imageSizes` is the multi-resolution variant
|
|
@@ -88,18 +116,73 @@ export type PodcastSummary = z.infer<typeof podcastSummarySchema>;
|
|
|
88
116
|
* original external artwork URL as an absolute fallback when re-hosting has not
|
|
89
117
|
* run yet.
|
|
90
118
|
*/
|
|
119
|
+
/**
|
|
120
|
+
* Where a Syra-hosted episode's audio actually is.
|
|
121
|
+
*
|
|
122
|
+
* `url` is a PATH on the Syra API (`/api/podcasts/episodes/:id/audio`), not an
|
|
123
|
+
* absolute URL — resolve it with `SyraClient.episodeAudioUrl` rather than
|
|
124
|
+
* handing it to a player directly.
|
|
125
|
+
*/
|
|
126
|
+
export const episodeAudioSourceSchema = z.object({
|
|
127
|
+
url: z.string(),
|
|
128
|
+
format: z.string().optional(),
|
|
129
|
+
bitrate: z.number().optional(),
|
|
130
|
+
duration: z.number().optional(),
|
|
131
|
+
});
|
|
132
|
+
export type EpisodeAudioSource = z.infer<typeof episodeAudioSourceSchema>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* An episode's processing state. `ready` is playable; `processing` is an episode
|
|
136
|
+
* whose audio is still being packaged (including one drafted but not yet
|
|
137
|
+
* ingested), and only its OWNER is shown those.
|
|
138
|
+
*/
|
|
139
|
+
export const episodeStatusSchema = z.enum(['ready', 'processing', 'failed', 'unavailable']);
|
|
140
|
+
export type EpisodeStatus = z.infer<typeof episodeStatusSchema>;
|
|
141
|
+
|
|
91
142
|
export const episodeSummarySchema = z.object({
|
|
92
143
|
id: z.string(),
|
|
93
144
|
podcastId: z.string(),
|
|
94
145
|
title: z.string(),
|
|
95
146
|
description: z.string().optional(),
|
|
96
|
-
|
|
147
|
+
/** ABSOLUTE, and present only for RSS-mirrored episodes — see the note above. */
|
|
148
|
+
enclosureUrl: z.string().optional(),
|
|
97
149
|
enclosureType: z.string().optional(),
|
|
98
150
|
enclosureLength: z.number().optional(),
|
|
151
|
+
/** A PATH on the Syra API, present for Syra-hosted episodes. */
|
|
152
|
+
audioSource: episodeAudioSourceSchema.optional(),
|
|
99
153
|
duration: z.number().optional(),
|
|
100
154
|
pubDate: z.string().optional(),
|
|
155
|
+
season: z.number().optional(),
|
|
156
|
+
episodeNumber: z.number().optional(),
|
|
157
|
+
status: episodeStatusSchema.optional(),
|
|
158
|
+
/** Disclosure: this episode's content was machine-generated. */
|
|
159
|
+
aiGenerated: z.boolean().optional(),
|
|
101
160
|
image: z.string().optional(),
|
|
102
161
|
imageSizes: coverArtSizesSchema.optional(),
|
|
103
162
|
imageSourceUrl: z.string().optional(),
|
|
104
163
|
});
|
|
105
164
|
export type EpisodeSummary = z.infer<typeof episodeSummarySchema>;
|
|
165
|
+
|
|
166
|
+
/** What `createEpisodeDraft` hands back: the episode to fill, and the capability to fill it. */
|
|
167
|
+
export const episodeDraftSchema = z.object({
|
|
168
|
+
episodeId: z.string(),
|
|
169
|
+
ingestTicket: z.string(),
|
|
170
|
+
expiresAt: z.string(),
|
|
171
|
+
});
|
|
172
|
+
export type EpisodeDraft = z.infer<typeof episodeDraftSchema>;
|
|
173
|
+
|
|
174
|
+
/** What `getEpisodeStream` hands back: a tokenized HLS URL with its own deadline. */
|
|
175
|
+
export const episodeStreamSchema = z.object({
|
|
176
|
+
url: z.string(),
|
|
177
|
+
type: z.string(),
|
|
178
|
+
expiresAt: z.string().optional(),
|
|
179
|
+
});
|
|
180
|
+
export type EpisodeStream = z.infer<typeof episodeStreamSchema>;
|
|
181
|
+
|
|
182
|
+
/** What the image upload endpoint hands back. */
|
|
183
|
+
export const uploadedImageSchema = z.object({
|
|
184
|
+
id: z.string(),
|
|
185
|
+
primaryColor: z.string().optional(),
|
|
186
|
+
secondaryColor: z.string().optional(),
|
|
187
|
+
});
|
|
188
|
+
export type UploadedImage = z.infer<typeof uploadedImageSchema>;
|