index.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import { z } from 'zod';
  2. import { PortSchema } from '@/schemas/primitives';
  3. import { ExternalProxyEntrySchema } from './external-proxy';
  4. import { FinalMaskStreamSettingsSchema } from './finalmask';
  5. import { GrpcStreamSettingsSchema } from './grpc';
  6. import { HttpUpgradeStreamSettingsSchema } from './httpupgrade';
  7. import { HysteriaStreamSettingsSchema } from './hysteria';
  8. import { KcpStreamSettingsSchema } from './kcp';
  9. import { MasqueStreamSettingsSchema } from './masque';
  10. import { SockoptStreamSettingsSchema } from './sockopt';
  11. import { TcpStreamSettingsSchema } from './tcp';
  12. import { WsStreamSettingsSchema } from './ws';
  13. import { XDriveStreamSettingsSchema } from './xdrive';
  14. import { XHttpStreamSettingsSchema } from './xhttp';
  15. export * from './external-proxy';
  16. export * from './finalmask';
  17. export * from './grpc';
  18. export * from './httpupgrade';
  19. export * from './hysteria';
  20. export * from './kcp';
  21. export * from './masque';
  22. export * from './sockopt';
  23. export * from './tcp';
  24. export * from './ws';
  25. export * from './xdrive';
  26. export * from './xhttp';
  27. export const NetworkSchema = z.enum([
  28. 'tcp',
  29. 'kcp',
  30. 'ws',
  31. 'grpc',
  32. 'httpupgrade',
  33. 'xhttp',
  34. 'hysteria',
  35. 'xdrive',
  36. 'masque',
  37. ]);
  38. export type Network = z.infer<typeof NetworkSchema>;
  39. // Tagged-wrapper DU on `network`. The wire shape uses an asymmetric per-
  40. // network key (`tcpSettings`, `wsSettings`, ...) rather than a single
  41. // `settings` object — same pattern Xray ships and the panel's StreamSettings
  42. // class flattens via toJson. Each branch carries only the matching key so
  43. // fixtures round-trip byte-identical.
  44. //
  45. // `hysteria` is only valid when the parent protocol is hysteria — the
  46. // network selector hides it for other protocols. xray-core enforces
  47. // the constraint server-side too.
  48. const TransportNetworkSettingsSchema = z.discriminatedUnion('network', [
  49. z.object({ network: z.literal('tcp'), tcpSettings: TcpStreamSettingsSchema }),
  50. z.object({ network: z.literal('kcp'), kcpSettings: KcpStreamSettingsSchema }),
  51. z.object({ network: z.literal('ws'), wsSettings: WsStreamSettingsSchema }),
  52. z.object({ network: z.literal('grpc'), grpcSettings: GrpcStreamSettingsSchema }),
  53. z.object({
  54. network: z.literal('httpupgrade'),
  55. httpupgradeSettings: HttpUpgradeStreamSettingsSchema,
  56. }),
  57. z.object({ network: z.literal('xhttp'), xhttpSettings: XHttpStreamSettingsSchema }),
  58. z.object({ network: z.literal('hysteria'), hysteriaSettings: HysteriaStreamSettingsSchema }),
  59. // `masque` pairs only with the masque protocol, in both directions.
  60. z.object({ network: z.literal('masque'), masqueSettings: MasqueStreamSettingsSchema }),
  61. // The stream-level address/port is the domain front XDRIVE dials for its storage API.
  62. z.object({
  63. network: z.literal('xdrive'),
  64. xdriveSettings: XDriveStreamSettingsSchema,
  65. address: z.string().optional(),
  66. port: PortSchema.optional(),
  67. }),
  68. ]);
  69. // Wireguard (always a UDP listener) and Tunnel (dokodemo-door) expose no
  70. // user-selectable transport: their streamSettings carries no `network` key —
  71. // only security/sockopt, and Tunnel relies on `sockopt.tproxy` for its TProxy
  72. // mode. The transportless branch accepts that shape (network absent), while a
  73. // present-but-invalid network still fails both branches so a typo can't slip
  74. // through. `network: never().optional()` reads as "this key must be absent".
  75. //
  76. // The preprocess folds `method` — xray-core v26.7.11's preferred alias for
  77. // `network`, which wins over `network` when both are present — back into the
  78. // panel-canonical `network` key, so imported/pasted configs keyed on the
  79. // alias don't silently match the transportless branch and lose their
  80. // transport.
  81. export const NetworkSettingsSchema = z.preprocess(
  82. (val) => {
  83. if (val && typeof val === 'object' && 'method' in val) {
  84. const { method, ...rest } = val as Record<string, unknown>;
  85. if (typeof method === 'string' && method !== '') {
  86. return { ...rest, network: method };
  87. }
  88. return rest;
  89. }
  90. return val;
  91. },
  92. z.union([TransportNetworkSettingsSchema, z.object({ network: z.never().optional() })]),
  93. );
  94. export type NetworkSettings = z.infer<typeof NetworkSettingsSchema>;
  95. // Orthogonal extras that ride alongside the network and security branches.
  96. // All optional on the wire — legacy toJson() omits any field whose value
  97. // is empty. The shadow harness treats absent and empty-array as the same
  98. // canonical state.
  99. export const StreamExtrasSchema = z.object({
  100. externalProxy: z.array(ExternalProxyEntrySchema).optional(),
  101. finalmask: FinalMaskStreamSettingsSchema.optional(),
  102. sockopt: SockoptStreamSettingsSchema.optional(),
  103. });
  104. export type StreamExtras = z.infer<typeof StreamExtrasSchema>;