@antiphony/shared 0.5.1 → 0.6.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.
@@ -80,16 +80,16 @@ declare const ProcessingRequestSchema: z.ZodObject<{
80
80
  */
81
81
  reprocess: z.ZodOptional<z.ZodBoolean>;
82
82
  }, "strip", z.ZodTypeAny, {
83
+ waveform?: boolean | undefined;
84
+ transcribe?: boolean | undefined;
83
85
  denoise?: boolean | undefined;
84
86
  trim?: boolean | undefined;
85
- transcribe?: boolean | undefined;
86
- waveform?: boolean | undefined;
87
87
  reprocess?: boolean | undefined;
88
88
  }, {
89
+ waveform?: boolean | undefined;
90
+ transcribe?: boolean | undefined;
89
91
  denoise?: boolean | undefined;
90
92
  trim?: boolean | undefined;
91
- transcribe?: boolean | undefined;
92
- waveform?: boolean | undefined;
93
93
  reprocess?: boolean | undefined;
94
94
  }>;
95
95
  type ProcessingRequest = z.infer<typeof ProcessingRequestSchema>;
@@ -104,15 +104,15 @@ declare const ProcessingStageMapSchema: z.ZodObject<{
104
104
  trim: z.ZodOptional<z.ZodEnum<["pending", "ready", "failed", "skipped"]>>;
105
105
  waveform: z.ZodOptional<z.ZodEnum<["pending", "ready", "failed", "skipped"]>>;
106
106
  }, "strip", z.ZodTypeAny, {
107
+ waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
108
+ transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
107
109
  denoise?: "pending" | "ready" | "failed" | "skipped" | undefined;
108
110
  trim?: "pending" | "ready" | "failed" | "skipped" | undefined;
109
- transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
110
- waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
111
111
  }, {
112
+ waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
113
+ transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
112
114
  denoise?: "pending" | "ready" | "failed" | "skipped" | undefined;
113
115
  trim?: "pending" | "ready" | "failed" | "skipped" | undefined;
114
- transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
115
- waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
116
116
  }>;
117
117
  type ProcessingStageMap = z.infer<typeof ProcessingStageMapSchema>;
118
118
  /**
@@ -133,16 +133,16 @@ declare const ResolvedProcessingSchema: z.ZodObject<{
133
133
  } & {
134
134
  reprocess: z.ZodOptional<z.ZodBoolean>;
135
135
  }, "strip", z.ZodTypeAny, {
136
+ waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
137
+ transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
136
138
  denoise?: "pending" | "ready" | "failed" | "skipped" | undefined;
137
139
  trim?: "pending" | "ready" | "failed" | "skipped" | undefined;
138
- transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
139
- waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
140
140
  reprocess?: boolean | undefined;
141
141
  }, {
142
+ waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
143
+ transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
142
144
  denoise?: "pending" | "ready" | "failed" | "skipped" | undefined;
143
145
  trim?: "pending" | "ready" | "failed" | "skipped" | undefined;
144
- transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
145
- waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
146
146
  reprocess?: boolean | undefined;
147
147
  }>;
148
148
  type ResolvedProcessing = z.infer<typeof ResolvedProcessingSchema>;
@@ -245,10 +245,10 @@ declare const ProcessingStateSchema: z.ZodObject<{
245
245
  updatedAt: z.ZodEffects<z.ZodUnion<[z.ZodType<unknown, z.ZodTypeDef, unknown>, z.ZodString, z.ZodNumber, z.ZodDate]>, Date, unknown>;
246
246
  }, "strip", z.ZodTypeAny, {
247
247
  updatedAt: Date;
248
+ waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
249
+ transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
248
250
  denoise?: "pending" | "ready" | "failed" | "skipped" | undefined;
249
251
  trim?: "pending" | "ready" | "failed" | "skipped" | undefined;
250
- transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
251
- waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
252
252
  reprocess?: boolean | undefined;
253
253
  processedBlobCid?: string | undefined;
254
254
  processedMimeType?: string | undefined;
@@ -257,10 +257,10 @@ declare const ProcessingStateSchema: z.ZodObject<{
257
257
  waveformPeaks?: number[] | undefined;
258
258
  leaseUntil?: Date | undefined;
259
259
  }, {
260
+ waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
261
+ transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
260
262
  denoise?: "pending" | "ready" | "failed" | "skipped" | undefined;
261
263
  trim?: "pending" | "ready" | "failed" | "skipped" | undefined;
262
- transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
263
- waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
264
264
  reprocess?: boolean | undefined;
265
265
  processedBlobCid?: string | undefined;
266
266
  processedMimeType?: string | undefined;
@@ -282,15 +282,15 @@ declare const ProcessingViewSchema: z.ZodObject<{
282
282
  trim: z.ZodOptional<z.ZodEnum<["pending", "ready", "failed", "skipped"]>>;
283
283
  waveform: z.ZodOptional<z.ZodEnum<["pending", "ready", "failed", "skipped"]>>;
284
284
  }, "strip", z.ZodTypeAny, {
285
+ waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
286
+ transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
285
287
  denoise?: "pending" | "ready" | "failed" | "skipped" | undefined;
286
288
  trim?: "pending" | "ready" | "failed" | "skipped" | undefined;
287
- transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
288
- waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
289
289
  }, {
290
+ waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
291
+ transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
290
292
  denoise?: "pending" | "ready" | "failed" | "skipped" | undefined;
291
293
  trim?: "pending" | "ready" | "failed" | "skipped" | undefined;
292
- transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
293
- waveform?: "pending" | "ready" | "failed" | "skipped" | undefined;
294
294
  }>;
295
295
  type ProcessingView = z.infer<typeof ProcessingViewSchema>;
296
296
  /**
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+
5
+ // types/url.ts
6
+ function httpsUrl() {
7
+ return zod.z.string().trim().url().regex(/^https?:\/\//i, { message: "URL must use the http or https scheme" });
8
+ }
9
+
10
+ exports.httpsUrl = httpsUrl;
@@ -0,0 +1,70 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Scheme-restricted URL schema: an absolute `http:`/`https:` URL, trimmed.
5
+ *
6
+ * ## Why this exists
7
+ *
8
+ * `z.string().url()` is `new URL()` under the hood, and `new URL()` accepts
9
+ * ANY scheme. All of these parse clean through a bare `.url()`:
10
+ *
11
+ * javascript:alert(1)
12
+ * data:text/html,<script>alert(1)</script>
13
+ * file:///etc/passwd
14
+ * vbscript:msgbox(1)
15
+ * blob:https://evil.example/x
16
+ *
17
+ * That is harmless for a value nobody dereferences and dangerous for one that
18
+ * clients do. `AudioEmbedView.url` is the field a player puts in `<audio src>`;
19
+ * `ActorProfileRecord.rssFeed` is the field an app renders as `<a href>`. A URL
20
+ * that reaches either of those unchecked is a stored-XSS shape — written once,
21
+ * fired for every viewer afterwards.
22
+ *
23
+ * So every URL field in this contract goes through this helper, and none of
24
+ * them uses a bare `.url()`. The `antiphony/no-bare-zod-url` ESLint rule
25
+ * enforces that, because "remember to use the helper" is not a control.
26
+ *
27
+ * ## Why `http:` is allowed, despite the name
28
+ *
29
+ * The name says https because https is what a deployment should serve; the rule
30
+ * admits `http:` because a self-hosted or local Antiphony is reached at
31
+ * `http://localhost:8787`, and `AudioEmbedView.url` is built from exactly that
32
+ * base (`ANTIPHONY_PUBLIC_BASE_URL`). Excluding plain http would make the
33
+ * contract unparseable in development for no safety gained: what this closes is
34
+ * scheme confusion, not transport confidentiality. Transport is a deployment
35
+ * concern, and the deployment that matters is https already.
36
+ *
37
+ * ## Why a string check and not a `.refine()`
38
+ *
39
+ * Two reasons, both practical:
40
+ *
41
+ * - **It stays a `ZodString`.** A `.refine()` wraps the schema in `ZodEffects`,
42
+ * which costs consumers `.extend()`-friendliness and makes the OpenAPI
43
+ * generator emit a weaker schema. As a plain string check the constraint
44
+ * survives into `openapi.json` as a `pattern`, so a consumer in another
45
+ * language reads the rule instead of having to already know it.
46
+ * - **It is checked against the raw string, not a parsed `protocol`.** HTML
47
+ * strips tabs and newlines out of URL attributes before dereferencing them,
48
+ * so a scheme split by a tab is a live `javascript:` URL in a browser — and
49
+ * one `new URL()` parses happily. Requiring the string to LITERALLY begin
50
+ * `http://` or `https://` rejects that whole family without enumerating it.
51
+ *
52
+ * `.trim()` runs first so surrounding whitespace is normalised away rather than
53
+ * sneaking a scheme past the anchor. It is a normalisation, not a loosening:
54
+ * bare `.url()` accepted padded input too, it just kept the padding.
55
+ *
56
+ * One fidelity note on that generated `pattern`: JSON Schema has no way to
57
+ * express a case-insensitive regex, so `openapi.json` documents
58
+ * `^https?:\/\/` without the `i` flag. `HTTPS://x` therefore passes here and
59
+ * fails a strict reading of the document. Real URLs carry a lowercase scheme,
60
+ * and the alternative — dropping `i` — would reject input for cosmetics, so the
61
+ * runtime stays the more permissive of the two on that one axis.
62
+ *
63
+ * Usage:
64
+ *
65
+ * url: httpsUrl()
66
+ * rssFeed: httpsUrl().optional()
67
+ */
68
+ declare function httpsUrl(): z.ZodString;
69
+
70
+ export { httpsUrl };
@@ -1,6 +1,7 @@
1
- export { CreateAudioPostRequestSchema, PatchAudioPostRequestSchema } from './chunk-UYCXRNTK.js';
2
- import './chunk-J2HE6PE2.js';
1
+ export { CreateAudioPostRequestSchema, PatchAudioPostRequestSchema } from './chunk-6EJLU4LG.js';
2
+ import './chunk-QQ5GOLYW.js';
3
+ import './chunk-SMK4OZNU.js';
3
4
  import './chunk-AKIWUNNK.js';
4
5
  import './chunk-D655OH2I.js';
5
- import './chunk-SMK4OZNU.js';
6
+ import './chunk-KMQWUFMV.js';
6
7
  import './chunk-5JBD5THX.js';
@@ -1,4 +1,4 @@
1
- import { ReplyRefSchema, AudioEmbedSchema } from './chunk-J2HE6PE2.js';
1
+ import { ReplyRefSchema, AudioEmbedSchema } from './chunk-QQ5GOLYW.js';
2
2
  import { ProcessingRequestSchema } from './chunk-AKIWUNNK.js';
3
3
  import { z } from 'zod';
4
4
 
@@ -12,9 +12,9 @@ var CreateAudioPostRequestSchema = z.object({
12
12
  /** Present ⇒ this is a reply (StrongRef root + parent). */
13
13
  reply: ReplyRefSchema.optional(),
14
14
  /** BCP-47 language tags. */
15
- langs: z.array(z.string()).max(3).optional(),
15
+ langs: z.array(z.string().max(35)).max(3).optional(),
16
16
  /** Author self-label values (content warnings). */
17
- selfLabels: z.array(z.string()).optional(),
17
+ selfLabels: z.array(z.string().max(128)).max(10).optional(),
18
18
  /**
19
19
  * Opt-in audio processing for this post's audio (denoise / trim /
20
20
  * transcribe / waveform). All default off. Stages the deployment can't
@@ -0,0 +1,8 @@
1
+ import { z } from 'zod';
2
+
3
+ // types/url.ts
4
+ function httpsUrl() {
5
+ return z.string().trim().url().regex(/^https?:\/\//i, { message: "URL must use the http or https scheme" });
6
+ }
7
+
8
+ export { httpsUrl };
@@ -1,11 +1,12 @@
1
+ import { BlobRefSchema } from './chunk-SMK4OZNU.js';
1
2
  import { ProcessingViewSchema, ProcessingStateSchema } from './chunk-AKIWUNNK.js';
2
3
  import { FirestoreTimestampSchema } from './chunk-D655OH2I.js';
3
- import { BlobRefSchema } from './chunk-SMK4OZNU.js';
4
+ import { httpsUrl } from './chunk-KMQWUFMV.js';
4
5
  import { z } from 'zod';
5
6
 
6
7
  var StrongRefSchema = z.object({
7
- uri: z.string().regex(/^at:\/\/.+/, "Must be an at:// URI"),
8
- cid: z.string()
8
+ uri: z.string().regex(/^at:\/\/.+/, "Must be an at:// URI").max(512),
9
+ cid: z.string().max(128)
9
10
  });
10
11
  var ReplyRefSchema = z.object({
11
12
  root: StrongRefSchema,
@@ -47,8 +48,13 @@ var AudioEmbedViewSchema = z.object({
47
48
  * it to change (trim removes leading/trailing silence), and should render
48
49
  * these three as a set rather than caching them independently. The record's
49
50
  * originals are immutable and unaffected; this is a read-time resolution.
51
+ *
52
+ * Restricted to `http:`/`https:` (`httpsUrl()`), not any URL `new URL()`
53
+ * will parse. This is the value clients hand to `<audio src>`, so a
54
+ * `javascript:` or `data:` URL reaching one is stored XSS — and a bare
55
+ * `.url()` accepts both. See `types/url.ts`.
50
56
  */
51
- url: z.string().url(),
57
+ url: httpsUrl(),
52
58
  durationMs: z.number().int().min(0).optional(),
53
59
  // `alt` is copied from the stored embed; keep the same bounds so a view can
54
60
  // never carry a larger payload than the record allows.
@@ -152,7 +158,13 @@ var TranscriptEnrichmentRecordSchema = z.object({
152
158
  var ActorProfileRecordSchema = z.object({
153
159
  handle: z.string().min(3).max(20).optional(),
154
160
  usageIntent: z.string().max(100).optional(),
155
- rssFeed: z.string().url().optional()
161
+ /**
162
+ * Feed URL, restricted to `http:`/`https:` for the same reason
163
+ * `AudioEmbedView.url` is: an app renders this as an `<a href>`, and a bare
164
+ * `.url()` would let a `javascript:` URL through. A feed is a fetchable web
165
+ * document, so no legitimate value loses anything to the restriction.
166
+ */
167
+ rssFeed: httpsUrl().optional()
156
168
  });
157
169
  var ViewerStateSchema = z.object({
158
170
  /** True when the authenticated caller authored this post. */
@@ -3,6 +3,7 @@ export { FirestoreTimestamp, FirestoreTimestampSchema } from './types/records.js
3
3
  export { ActorProfileRecord, ActorProfileRecordSchema, AudioEmbed, AudioEmbedSchema, AudioEmbedView, AudioEmbedViewSchema, AudioPostRecord, AudioPostRecordSchema, AudioPostView, AudioPostViewSchema, PostRecordPublic, PostRecordPublicSchema, ReplyRef, ReplyRefSchema, StrongRef, StrongRefSchema, TimedTranscript, TimedTranscriptSchema, TranscriptEnrichmentRecord, TranscriptEnrichmentRecordSchema, TranscriptSegment, TranscriptSegmentSchema, ViewerState, ViewerStateSchema } from './types/audio.js';
4
4
  export { BYTE_MUTATING_STAGES, CanonicalAudioFields, DERIVED_STAGES, PROCESSING_STAGES, ProcessingRequest, ProcessingRequestSchema, ProcessingStage, ProcessingStageMap, ProcessingStageMapSchema, ProcessingStageSchema, ProcessingStageStatus, ProcessingStageStatusSchema, ProcessingState, ProcessingStateSchema, ProcessingView, ProcessingViewSchema, ResolvedProcessing, ResolvedProcessingSchema, resolveAudioVariant, toProcessingView } from './types/processing.js';
5
5
  export { BlobRef, BlobRefSchema } from './types/blob.js';
6
+ export { httpsUrl } from './types/url.js';
6
7
  export { COLLECTIONS, EMBED_NSID, NSID, NsidValue, StoredNsidValue, XRPC_NSID, XrpcNsidValue } from './nsid.js';
7
8
  export { ConflictError, ForbiddenError, NotFoundError, RateLimitError, ServiceError, UnauthorizedError, ValidationError } from './errors/index.js';
8
9
  export { isFirestoreTimestamp } from './utils/index.js';
package/dist/esm/index.js CHANGED
@@ -1,11 +1,12 @@
1
- export { isFirestoreTimestamp } from './chunk-24KIXQZK.js';
2
- export { CreateAudioPostRequestSchema, PatchAudioPostRequestSchema } from './chunk-UYCXRNTK.js';
1
+ export { CreateAudioPostRequestSchema, PatchAudioPostRequestSchema } from './chunk-6EJLU4LG.js';
3
2
  export { COLLECTIONS, EMBED_NSID, NSID, XRPC_NSID } from './chunk-F5RKN3GY.js';
4
3
  export { ConflictError, ForbiddenError, NotFoundError, RateLimitError, ServiceError, UnauthorizedError, ValidationError } from './chunk-7LW2FHLD.js';
5
4
  import './chunk-72G3LBUQ.js';
6
5
  export { buildReportedErrorEvent, reportError } from './chunk-BNNLHRH7.js';
7
- export { ActorProfileRecordSchema, AudioEmbedSchema, AudioEmbedViewSchema, AudioPostRecordSchema, AudioPostViewSchema, PostRecordPublicSchema, ReplyRefSchema, StrongRefSchema, TimedTranscriptSchema, TranscriptEnrichmentRecordSchema, TranscriptSegmentSchema, ViewerStateSchema } from './chunk-J2HE6PE2.js';
6
+ export { isFirestoreTimestamp } from './chunk-24KIXQZK.js';
7
+ export { ActorProfileRecordSchema, AudioEmbedSchema, AudioEmbedViewSchema, AudioPostRecordSchema, AudioPostViewSchema, PostRecordPublicSchema, ReplyRefSchema, StrongRefSchema, TimedTranscriptSchema, TranscriptEnrichmentRecordSchema, TranscriptSegmentSchema, ViewerStateSchema } from './chunk-QQ5GOLYW.js';
8
+ export { BlobRefSchema } from './chunk-SMK4OZNU.js';
8
9
  export { BYTE_MUTATING_STAGES, DERIVED_STAGES, PROCESSING_STAGES, ProcessingRequestSchema, ProcessingStageMapSchema, ProcessingStageSchema, ProcessingStageStatusSchema, ProcessingStateSchema, ProcessingViewSchema, ResolvedProcessingSchema, resolveAudioVariant, toProcessingView } from './chunk-AKIWUNNK.js';
9
10
  export { FirestoreTimestampSchema } from './chunk-D655OH2I.js';
10
- export { BlobRefSchema } from './chunk-SMK4OZNU.js';
11
+ export { httpsUrl } from './chunk-KMQWUFMV.js';
11
12
  import './chunk-5JBD5THX.js';
@@ -228,6 +228,11 @@ declare const AudioEmbedViewSchema: z.ZodObject<{
228
228
  * it to change (trim removes leading/trailing silence), and should render
229
229
  * these three as a set rather than caching them independently. The record's
230
230
  * originals are immutable and unaffected; this is a read-time resolution.
231
+ *
232
+ * Restricted to `http:`/`https:` (`httpsUrl()`), not any URL `new URL()`
233
+ * will parse. This is the value clients hand to `<audio src>`, so a
234
+ * `javascript:` or `data:` URL reaching one is stored XSS — and a bare
235
+ * `.url()` accepts both. See `types/url.ts`.
231
236
  */
232
237
  url: z.ZodString;
233
238
  durationMs: z.ZodOptional<z.ZodNumber>;
@@ -870,6 +875,12 @@ type TranscriptEnrichmentRecord = z.infer<typeof TranscriptEnrichmentRecordSchem
870
875
  declare const ActorProfileRecordSchema: z.ZodObject<{
871
876
  handle: z.ZodOptional<z.ZodString>;
872
877
  usageIntent: z.ZodOptional<z.ZodString>;
878
+ /**
879
+ * Feed URL, restricted to `http:`/`https:` for the same reason
880
+ * `AudioEmbedView.url` is: an app renders this as an `<a href>`, and a bare
881
+ * `.url()` would let a `javascript:` URL through. A feed is a fetchable web
882
+ * document, so no legitimate value loses anything to the restriction.
883
+ */
873
884
  rssFeed: z.ZodOptional<z.ZodString>;
874
885
  }, "strip", z.ZodTypeAny, {
875
886
  handle?: string | undefined;
@@ -1107,6 +1118,11 @@ declare const AudioPostViewSchema: z.ZodObject<{
1107
1118
  * it to change (trim removes leading/trailing silence), and should render
1108
1119
  * these three as a set rather than caching them independently. The record's
1109
1120
  * originals are immutable and unaffected; this is a read-time resolution.
1121
+ *
1122
+ * Restricted to `http:`/`https:` (`httpsUrl()`), not any URL `new URL()`
1123
+ * will parse. This is the value clients hand to `<audio src>`, so a
1124
+ * `javascript:` or `data:` URL reaching one is stored XSS — and a bare
1125
+ * `.url()` accepts both. See `types/url.ts`.
1110
1126
  */
1111
1127
  url: z.ZodString;
1112
1128
  durationMs: z.ZodOptional<z.ZodNumber>;
@@ -1,5 +1,6 @@
1
- export { ActorProfileRecordSchema, AudioEmbedSchema, AudioEmbedViewSchema, AudioPostRecordSchema, AudioPostViewSchema, PostRecordPublicSchema, ReplyRefSchema, StrongRefSchema, TimedTranscriptSchema, TranscriptEnrichmentRecordSchema, TranscriptSegmentSchema, ViewerStateSchema } from '../chunk-J2HE6PE2.js';
1
+ export { ActorProfileRecordSchema, AudioEmbedSchema, AudioEmbedViewSchema, AudioPostRecordSchema, AudioPostViewSchema, PostRecordPublicSchema, ReplyRefSchema, StrongRefSchema, TimedTranscriptSchema, TranscriptEnrichmentRecordSchema, TranscriptSegmentSchema, ViewerStateSchema } from '../chunk-QQ5GOLYW.js';
2
+ import '../chunk-SMK4OZNU.js';
2
3
  import '../chunk-AKIWUNNK.js';
3
4
  import '../chunk-D655OH2I.js';
4
- import '../chunk-SMK4OZNU.js';
5
+ import '../chunk-KMQWUFMV.js';
5
6
  import '../chunk-5JBD5THX.js';
@@ -0,0 +1,70 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Scheme-restricted URL schema: an absolute `http:`/`https:` URL, trimmed.
5
+ *
6
+ * ## Why this exists
7
+ *
8
+ * `z.string().url()` is `new URL()` under the hood, and `new URL()` accepts
9
+ * ANY scheme. All of these parse clean through a bare `.url()`:
10
+ *
11
+ * javascript:alert(1)
12
+ * data:text/html,<script>alert(1)</script>
13
+ * file:///etc/passwd
14
+ * vbscript:msgbox(1)
15
+ * blob:https://evil.example/x
16
+ *
17
+ * That is harmless for a value nobody dereferences and dangerous for one that
18
+ * clients do. `AudioEmbedView.url` is the field a player puts in `<audio src>`;
19
+ * `ActorProfileRecord.rssFeed` is the field an app renders as `<a href>`. A URL
20
+ * that reaches either of those unchecked is a stored-XSS shape — written once,
21
+ * fired for every viewer afterwards.
22
+ *
23
+ * So every URL field in this contract goes through this helper, and none of
24
+ * them uses a bare `.url()`. The `antiphony/no-bare-zod-url` ESLint rule
25
+ * enforces that, because "remember to use the helper" is not a control.
26
+ *
27
+ * ## Why `http:` is allowed, despite the name
28
+ *
29
+ * The name says https because https is what a deployment should serve; the rule
30
+ * admits `http:` because a self-hosted or local Antiphony is reached at
31
+ * `http://localhost:8787`, and `AudioEmbedView.url` is built from exactly that
32
+ * base (`ANTIPHONY_PUBLIC_BASE_URL`). Excluding plain http would make the
33
+ * contract unparseable in development for no safety gained: what this closes is
34
+ * scheme confusion, not transport confidentiality. Transport is a deployment
35
+ * concern, and the deployment that matters is https already.
36
+ *
37
+ * ## Why a string check and not a `.refine()`
38
+ *
39
+ * Two reasons, both practical:
40
+ *
41
+ * - **It stays a `ZodString`.** A `.refine()` wraps the schema in `ZodEffects`,
42
+ * which costs consumers `.extend()`-friendliness and makes the OpenAPI
43
+ * generator emit a weaker schema. As a plain string check the constraint
44
+ * survives into `openapi.json` as a `pattern`, so a consumer in another
45
+ * language reads the rule instead of having to already know it.
46
+ * - **It is checked against the raw string, not a parsed `protocol`.** HTML
47
+ * strips tabs and newlines out of URL attributes before dereferencing them,
48
+ * so a scheme split by a tab is a live `javascript:` URL in a browser — and
49
+ * one `new URL()` parses happily. Requiring the string to LITERALLY begin
50
+ * `http://` or `https://` rejects that whole family without enumerating it.
51
+ *
52
+ * `.trim()` runs first so surrounding whitespace is normalised away rather than
53
+ * sneaking a scheme past the anchor. It is a normalisation, not a loosening:
54
+ * bare `.url()` accepted padded input too, it just kept the padding.
55
+ *
56
+ * One fidelity note on that generated `pattern`: JSON Schema has no way to
57
+ * express a case-insensitive regex, so `openapi.json` documents
58
+ * `^https?:\/\/` without the `i` flag. `HTTPS://x` therefore passes here and
59
+ * fails a strict reading of the document. Real URLs carry a lowercase scheme,
60
+ * and the alternative — dropping `i` — would reject input for cosmetics, so the
61
+ * runtime stays the more permissive of the two on that one axis.
62
+ *
63
+ * Usage:
64
+ *
65
+ * url: httpsUrl()
66
+ * rssFeed: httpsUrl().optional()
67
+ */
68
+ declare function httpsUrl(): z.ZodString;
69
+
70
+ export { httpsUrl };
@@ -0,0 +1,2 @@
1
+ export { httpsUrl } from '../chunk-KMQWUFMV.js';
2
+ import '../chunk-5JBD5THX.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antiphony/shared",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Shared types, Zod schemas, and codecs for Antiphony — open audio call-and-response infrastructure and AT Protocol lexicons.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.antiphony.dev",
@@ -137,14 +137,16 @@
137
137
  "lint": "eslint . --max-warnings=0",
138
138
  "test": "vitest run"
139
139
  },
140
- "dependencies": {
140
+ "//peerDependencies": "zod is a PEER, not a dependency, and the build has always assumed as much: tsup.config.ts marks it `external` so that there is ONE zod module instance shared with the consumer — the comment there spells out the consequence, that `@hono/zod-openapi`'s `.openapi()` extension never reaches schemas defined here if the instance is duplicated. Declaring it under `dependencies` said the opposite, that this package brings its own, and the contradiction was not theoretical: a consumer pinned to zod 3.22.4 against this package's `^3.24.1` gets TWO instances, 3.22.4 hoisted and 3.25.76 nested here. That is a live hazard inside zod 3, not a hedge against zod 4. As a peer, npm resolves one instance and says so when it cannot. The range is `^3.24.1` and is deliberately NOT widened to include `^4.0.0`: the schemas here are written against the zod 3 API, #148 verified that a zod 4 consumer breaks at parse time, and a peer range that overstates what the code supports is exactly the failure mode `@hono/zod-openapi` demonstrated with its `zod: >=3.0.0` — a range npm cannot fault, since 3.25.76 satisfies it, while the code underneath needed `^3.20.2`. Widen it only when the schemas actually move. ⚠️ SHIPS IN 0.6.0, which is unreleased — this changes the install contract, so a consumer that did not declare zod itself must now do so (npm 7+ auto-installs peers, but a consumer with `legacy-peer-deps` set does not). Vox Pop already declares zod in every workspace, so it is a no-op there.",
141
+ "peerDependencies": {
141
142
  "zod": "^3.24.1"
142
143
  },
143
144
  "devDependencies": {
144
145
  "@eslint/js": "^9.39.2",
145
146
  "tsup": "^8.5.1",
146
- "typescript-eslint": "^8.18.0",
147
+ "typescript-eslint": "^8.70.0",
147
148
  "typescript": "^5.7.2",
148
- "vitest": "^4.1.9"
149
+ "vitest": "^4.1.9",
150
+ "zod": "^3.24.1"
149
151
  }
150
152
  }