@appforge-ci/core 0.2.3 → 0.3.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.
Files changed (60) hide show
  1. package/dist/agent-readiness.d.ts +232 -1
  2. package/dist/agent-readiness.d.ts.map +1 -1
  3. package/dist/agent-readiness.js +294 -1
  4. package/dist/agent-readiness.js.map +1 -1
  5. package/dist/agent-readiness.test.js +253 -19
  6. package/dist/agent-readiness.test.js.map +1 -1
  7. package/dist/api-client.d.ts +42 -3
  8. package/dist/api-client.d.ts.map +1 -1
  9. package/dist/api-client.js +27 -0
  10. package/dist/api-client.js.map +1 -1
  11. package/dist/api-client.test.js +22 -0
  12. package/dist/api-client.test.js.map +1 -1
  13. package/dist/attestation.d.ts +265 -0
  14. package/dist/attestation.d.ts.map +1 -0
  15. package/dist/attestation.js +327 -0
  16. package/dist/attestation.js.map +1 -0
  17. package/dist/attestation.test.d.ts +2 -0
  18. package/dist/attestation.test.d.ts.map +1 -0
  19. package/dist/attestation.test.js +218 -0
  20. package/dist/attestation.test.js.map +1 -0
  21. package/dist/build-vpn.d.ts +10 -0
  22. package/dist/build-vpn.d.ts.map +1 -0
  23. package/dist/build-vpn.js +23 -0
  24. package/dist/build-vpn.js.map +1 -0
  25. package/dist/index.d.ts +5 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +5 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/schemas.d.ts +342 -7
  30. package/dist/schemas.d.ts.map +1 -1
  31. package/dist/schemas.js +92 -2
  32. package/dist/schemas.js.map +1 -1
  33. package/dist/types.d.ts +149 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js.map +1 -1
  36. package/dist/vpn-egress-presets.d.ts +142 -0
  37. package/dist/vpn-egress-presets.d.ts.map +1 -0
  38. package/dist/vpn-egress-presets.js +318 -0
  39. package/dist/vpn-egress-presets.js.map +1 -0
  40. package/dist/vpn-egress-presets.test.d.ts +2 -0
  41. package/dist/vpn-egress-presets.test.d.ts.map +1 -0
  42. package/dist/vpn-egress-presets.test.js +210 -0
  43. package/dist/vpn-egress-presets.test.js.map +1 -0
  44. package/dist/vpn-egress.d.ts +57 -0
  45. package/dist/vpn-egress.d.ts.map +1 -0
  46. package/dist/vpn-egress.js +126 -0
  47. package/dist/vpn-egress.js.map +1 -0
  48. package/dist/vpn-egress.test.d.ts +2 -0
  49. package/dist/vpn-egress.test.d.ts.map +1 -0
  50. package/dist/vpn-egress.test.js +133 -0
  51. package/dist/vpn-egress.test.js.map +1 -0
  52. package/dist/vpn-ssh.d.ts +45 -0
  53. package/dist/vpn-ssh.d.ts.map +1 -0
  54. package/dist/vpn-ssh.js +108 -0
  55. package/dist/vpn-ssh.js.map +1 -0
  56. package/dist/vpn-ssh.test.d.ts +2 -0
  57. package/dist/vpn-ssh.test.d.ts.map +1 -0
  58. package/dist/vpn-ssh.test.js +75 -0
  59. package/dist/vpn-ssh.test.js.map +1 -0
  60. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-vpn.js","sourceRoot":"","sources":["../src/build-vpn.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAA+B;IAC9D,IAAI,CAAC,GAAG;QAAE,OAAO,0BAA0B,CAAC;IAC5C,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,IAAI,QAAQ,CAAC;IACxC,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;QACb,IAAI,GAAG,CAAC,OAAO,KAAK,WAAW;YAAE,OAAO,qBAAqB,IAAI,cAAc,CAAC;QAChF,IAAI,GAAG,CAAC,OAAO,KAAK,QAAQ;YAAE,OAAO,sBAAsB,GAAG,CAAC,MAAM,IAAI,gCAAgC,EAAE,CAAC;QAC5G,OAAO,kBAAkB,IAAI,oCAAoC,CAAC;IACpE,CAAC;IACD,IAAI,GAAG,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,gEAAgE,CAAC;IACvG,OAAO,QAAQ,CAAC;AAClB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -6,5 +6,10 @@ export * from "./ota-rollout.js";
6
6
  export * from "./ota-bundle-hash.js";
7
7
  export * from "./ota-artifact.js";
8
8
  export * from "./vpn-network.js";
9
+ export * from "./vpn-egress.js";
10
+ export * from "./vpn-ssh.js";
11
+ export * from "./vpn-egress-presets.js";
9
12
  export * from "./artifact-upload.js";
13
+ export * from "./build-vpn.js";
14
+ export * from "./attestation.js";
10
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -6,5 +6,10 @@ export * from "./ota-rollout.js";
6
6
  export * from "./ota-bundle-hash.js";
7
7
  export * from "./ota-artifact.js";
8
8
  export * from "./vpn-network.js";
9
+ export * from "./vpn-egress.js";
10
+ export * from "./vpn-ssh.js";
11
+ export * from "./vpn-egress-presets.js";
9
12
  export * from "./artifact-upload.js";
13
+ export * from "./build-vpn.js";
14
+ export * from "./attestation.js";
10
15
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC"}
package/dist/schemas.d.ts CHANGED
@@ -492,14 +492,14 @@ export declare const updateAppSchema: z.ZodObject<{
492
492
  useVpn: z.ZodOptional<z.ZodBoolean>;
493
493
  }, "strip", z.ZodTypeAny, {
494
494
  name?: string | undefined;
495
+ useVpn?: boolean | undefined;
495
496
  repoUrl?: string | undefined;
496
497
  defaultBranch?: string | undefined;
497
- useVpn?: boolean | undefined;
498
498
  }, {
499
499
  name?: string | undefined;
500
+ useVpn?: boolean | undefined;
500
501
  repoUrl?: string | undefined;
501
502
  defaultBranch?: string | undefined;
502
- useVpn?: boolean | undefined;
503
503
  }>;
504
504
  export declare const updateOrgSchema: z.ZodObject<{
505
505
  name: z.ZodOptional<z.ZodString>;
@@ -629,6 +629,20 @@ export declare const agentHeartbeatSchema: z.ZodObject<{
629
629
  * predates the VPN feature omits it and stays `vpnCapable = 0`, so it
630
630
  * is never offered a VPN build. */
631
631
  vpnCapable: z.ZodOptional<z.ZodBoolean>;
632
+ /** VPN features this agent can enforce (migrations/0043) — ids matching
633
+ * `VPN_FEATURE_ID_PATTERN`, at most `MAX_VPN_FEATURES`. Like `vpnCapable`
634
+ * the stored value is derived from THIS heartbeat alone: omitting it (an
635
+ * older agent) means "no features", never "keep the last value". A
636
+ * malformed list is rejected (400) rather than half-trusted. */
637
+ vpnFeatures: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
638
+ /** Build-isolation level this agent enforces (migrations/0043). Same
639
+ * omit-means-default (`none`) rule as `vpnFeatures`. */
640
+ enforcementLevel: z.ZodOptional<z.ZodEnum<["none", "sandbox", "sandbox_uid"]>>;
641
+ /** Whether a GitHub Actions self-hosted runner shares this machine
642
+ * (migrations/0047). Like `vpnFeatures`, stored from THIS heartbeat alone:
643
+ * omitted (an older agent, or a check that failed) means "unknown", never
644
+ * "keep the last value", and unknown refuses a dedicated host. */
645
+ coHostedRunner: z.ZodOptional<z.ZodBoolean>;
632
646
  }, "strip", z.ZodTypeAny, {
633
647
  status: "online" | "draining" | "building";
634
648
  xcodeVersion: string | null;
@@ -642,6 +656,9 @@ export declare const agentHeartbeatSchema: z.ZodObject<{
642
656
  agentVersion?: string | null | undefined;
643
657
  screenSharingEnabled?: boolean | null | undefined;
644
658
  vpnCapable?: boolean | undefined;
659
+ vpnFeatures?: string[] | undefined;
660
+ enforcementLevel?: "none" | "sandbox" | "sandbox_uid" | undefined;
661
+ coHostedRunner?: boolean | undefined;
645
662
  tailscaleHostname?: string | null | undefined;
646
663
  currentBuildId?: string | null | undefined;
647
664
  activeBuildIds?: string[] | undefined;
@@ -659,6 +676,9 @@ export declare const agentHeartbeatSchema: z.ZodObject<{
659
676
  agentVersion?: string | null | undefined;
660
677
  screenSharingEnabled?: boolean | null | undefined;
661
678
  vpnCapable?: boolean | undefined;
679
+ vpnFeatures?: string[] | undefined;
680
+ enforcementLevel?: "none" | "sandbox" | "sandbox_uid" | undefined;
681
+ coHostedRunner?: boolean | undefined;
662
682
  tailscaleHostname?: string | null | undefined;
663
683
  currentBuildId?: string | null | undefined;
664
684
  activeBuildIds?: string[] | undefined;
@@ -668,11 +688,11 @@ export declare const appendLogSchema: z.ZodObject<{
668
688
  chunk: z.ZodString;
669
689
  seq: z.ZodNumber;
670
690
  }, "strip", z.ZodTypeAny, {
671
- chunk: string;
672
691
  seq: number;
673
- }, {
674
692
  chunk: string;
693
+ }, {
675
694
  seq: number;
695
+ chunk: string;
676
696
  }>;
677
697
  export declare const completeBuildSchema: z.ZodObject<{
678
698
  status: z.ZodEnum<["succeeded", "failed"]>;
@@ -737,8 +757,8 @@ export declare const promoteOtaReleaseSchema: z.ZodObject<{
737
757
  rollout: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
738
758
  }, "strip", z.ZodTypeAny, {
739
759
  channel: string;
740
- mandatory: boolean;
741
760
  buildId: string;
761
+ mandatory: boolean;
742
762
  rollout: number;
743
763
  }, {
744
764
  buildId: string;
@@ -785,6 +805,29 @@ export declare const setAgentVpnApprovedSchema: z.ZodObject<{
785
805
  }, {
786
806
  approved: boolean;
787
807
  }>;
808
+ /** `PATCH /orgs/:orgId/agents/:agentId/dedicated` (apps/admin-api) — the
809
+ * only way `Agent.dedicatedOrgId` ever changes (migrations/0043). An
810
+ * operator taint, deliberately not in agentHeartbeatSchema/agentEnrollSchema:
811
+ * an agent must never be able to widen or lift its own restriction. `null`
812
+ * clears it (the Mac goes back to being shared). */
813
+ export declare const setAgentDedicatedOrgSchema: z.ZodObject<{
814
+ dedicatedOrgId: z.ZodNullable<z.ZodString>;
815
+ }, "strip", z.ZodTypeAny, {
816
+ dedicatedOrgId: string | null;
817
+ }, {
818
+ dedicatedOrgId: string | null;
819
+ }>;
820
+ /** `PATCH /orgs/:orgId/vpn-hosts/:tunnelOrgId/policy` (apps/admin-api, platform
821
+ * admins only) — the only way `VpnTunnel.hostPolicy` ever changes
822
+ * (migrations/0047). Deliberately not in `updateVpnTunnelSchema`: an org
823
+ * admin must not be able to loosen the placement the operator set. */
824
+ export declare const setVpnHostPolicySchema: z.ZodObject<{
825
+ hostPolicy: z.ZodEnum<["shared", "pinned", "dedicated"]>;
826
+ }, "strip", z.ZodTypeAny, {
827
+ hostPolicy: "shared" | "pinned" | "dedicated";
828
+ }, {
829
+ hostPolicy: "shared" | "pinned" | "dedicated";
830
+ }>;
788
831
  /** `PATCH /orgs/:orgId/agents/:agentId/device-stream-hostname` (apps/
789
832
  * admin-api)'s request body — `hostname: null` clears it, going back
790
833
  * to the global DEVICE_STREAM_WS_URL fallback (see migrations/0034's
@@ -812,7 +855,159 @@ export declare const startVncSessionSchema: z.ZodObject<{
812
855
  }, {
813
856
  mode?: "view" | "control" | undefined;
814
857
  }>;
815
- export declare const createVpnTunnelSchema: z.ZodObject<{
858
+ /**
859
+ * The shape-only form of the create/update bodies, WITHOUT the egress rules. The
860
+ * API route parses with these and then runs `resolveEgressChange`, which applies
861
+ * the same `validateEgressSettings` but answers with a structured 400 listing
862
+ * every problem (`errors`) rather than a generic schema error. Anything else
863
+ * should use `createVpnTunnelSchema` / `updateVpnTunnelSchema` below.
864
+ */
865
+ export declare const createVpnTunnelBodySchema: z.ZodObject<{
866
+ /** `regulated` needs `strictEgress`, and the operator's REGULATED_VPN_ENABLED flag. */
867
+ complianceMode: z.ZodOptional<z.ZodEnum<["standard", "regulated"]>>;
868
+ strictEgress: z.ZodOptional<z.ZodBoolean>;
869
+ /** Preset ids; replaces the list. */
870
+ egressPresets: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
871
+ /** Custom hosts (exact or `*.suffix`, port 443 unless `ports` says otherwise); replaces the list. */
872
+ egressAllow: z.ZodOptional<z.ZodArray<z.ZodObject<{
873
+ host: z.ZodString;
874
+ ports: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
875
+ }, "strip", z.ZodTypeAny, {
876
+ host: string;
877
+ ports?: number[] | undefined;
878
+ }, {
879
+ host: string;
880
+ ports?: number[] | undefined;
881
+ }>, "many">>;
882
+ name: z.ZodString;
883
+ gatewayEndpoint: z.ZodString;
884
+ gatewayPublicKey: z.ZodString;
885
+ lanCidrs: z.ZodArray<z.ZodString, "many">;
886
+ dnsServer: z.ZodOptional<z.ZodNullable<z.ZodString>>;
887
+ allowedDomains: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
888
+ /** Omit to have the server allocate a free /30. Supply it only when a
889
+ * customer network already routes the allocated range. */
890
+ tunnelCidr: z.ZodOptional<z.ZodString>;
891
+ /** Lab mode — allow a private (RFC1918/CGNAT) gateway endpoint. Setting it
892
+ * `true` is platform-admin only (enforced by the route, not here). */
893
+ allowPrivateEndpoint: z.ZodOptional<z.ZodBoolean>;
894
+ }, "strip", z.ZodTypeAny, {
895
+ name: string;
896
+ gatewayEndpoint: string;
897
+ gatewayPublicKey: string;
898
+ lanCidrs: string[];
899
+ egressAllow?: {
900
+ host: string;
901
+ ports?: number[] | undefined;
902
+ }[] | undefined;
903
+ egressPresets?: string[] | undefined;
904
+ dnsServer?: string | null | undefined;
905
+ allowedDomains?: string[] | undefined;
906
+ tunnelCidr?: string | undefined;
907
+ allowPrivateEndpoint?: boolean | undefined;
908
+ complianceMode?: "standard" | "regulated" | undefined;
909
+ strictEgress?: boolean | undefined;
910
+ }, {
911
+ name: string;
912
+ gatewayEndpoint: string;
913
+ gatewayPublicKey: string;
914
+ lanCidrs: string[];
915
+ egressAllow?: {
916
+ host: string;
917
+ ports?: number[] | undefined;
918
+ }[] | undefined;
919
+ egressPresets?: string[] | undefined;
920
+ dnsServer?: string | null | undefined;
921
+ allowedDomains?: string[] | undefined;
922
+ tunnelCidr?: string | undefined;
923
+ allowPrivateEndpoint?: boolean | undefined;
924
+ complianceMode?: "standard" | "regulated" | undefined;
925
+ strictEgress?: boolean | undefined;
926
+ }>;
927
+ export declare const updateVpnTunnelBodySchema: z.ZodObject<{
928
+ /** `regulated` needs `strictEgress`, and the operator's REGULATED_VPN_ENABLED flag. */
929
+ complianceMode: z.ZodOptional<z.ZodEnum<["standard", "regulated"]>>;
930
+ strictEgress: z.ZodOptional<z.ZodBoolean>;
931
+ /** Preset ids; replaces the list. */
932
+ egressPresets: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
933
+ /** Custom hosts (exact or `*.suffix`, port 443 unless `ports` says otherwise); replaces the list. */
934
+ egressAllow: z.ZodOptional<z.ZodArray<z.ZodObject<{
935
+ host: z.ZodString;
936
+ ports: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
937
+ }, "strip", z.ZodTypeAny, {
938
+ host: string;
939
+ ports?: number[] | undefined;
940
+ }, {
941
+ host: string;
942
+ ports?: number[] | undefined;
943
+ }>, "many">>;
944
+ name: z.ZodOptional<z.ZodString>;
945
+ gatewayEndpoint: z.ZodOptional<z.ZodString>;
946
+ gatewayPublicKey: z.ZodOptional<z.ZodString>;
947
+ lanCidrs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
948
+ /** Explicit `null` clears it; omitted leaves it unchanged. */
949
+ dnsServer: z.ZodOptional<z.ZodNullable<z.ZodString>>;
950
+ allowedDomains: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
951
+ tunnelCidr: z.ZodOptional<z.ZodString>;
952
+ /** Lab mode. `true` is platform-admin only; omitted leaves it unchanged. */
953
+ allowPrivateEndpoint: z.ZodOptional<z.ZodBoolean>;
954
+ /** `false` disables the tunnel (builds that need it then fail fast
955
+ * instead of queueing); `true` re-enables it, back to `pending` until
956
+ * the next handshake proves it works. */
957
+ enabled: z.ZodOptional<z.ZodBoolean>;
958
+ }, "strip", z.ZodTypeAny, {
959
+ name?: string | undefined;
960
+ egressAllow?: {
961
+ host: string;
962
+ ports?: number[] | undefined;
963
+ }[] | undefined;
964
+ egressPresets?: string[] | undefined;
965
+ enabled?: boolean | undefined;
966
+ gatewayEndpoint?: string | undefined;
967
+ gatewayPublicKey?: string | undefined;
968
+ lanCidrs?: string[] | undefined;
969
+ dnsServer?: string | null | undefined;
970
+ allowedDomains?: string[] | undefined;
971
+ tunnelCidr?: string | undefined;
972
+ allowPrivateEndpoint?: boolean | undefined;
973
+ complianceMode?: "standard" | "regulated" | undefined;
974
+ strictEgress?: boolean | undefined;
975
+ }, {
976
+ name?: string | undefined;
977
+ egressAllow?: {
978
+ host: string;
979
+ ports?: number[] | undefined;
980
+ }[] | undefined;
981
+ egressPresets?: string[] | undefined;
982
+ enabled?: boolean | undefined;
983
+ gatewayEndpoint?: string | undefined;
984
+ gatewayPublicKey?: string | undefined;
985
+ lanCidrs?: string[] | undefined;
986
+ dnsServer?: string | null | undefined;
987
+ allowedDomains?: string[] | undefined;
988
+ tunnelCidr?: string | undefined;
989
+ allowPrivateEndpoint?: boolean | undefined;
990
+ complianceMode?: "standard" | "regulated" | undefined;
991
+ strictEgress?: boolean | undefined;
992
+ }>;
993
+ /** The create body, with the egress rules (bare `*`, IP literals, unknown presets, merged port limit) enforced too. */
994
+ export declare const createVpnTunnelSchema: z.ZodEffects<z.ZodObject<{
995
+ /** `regulated` needs `strictEgress`, and the operator's REGULATED_VPN_ENABLED flag. */
996
+ complianceMode: z.ZodOptional<z.ZodEnum<["standard", "regulated"]>>;
997
+ strictEgress: z.ZodOptional<z.ZodBoolean>;
998
+ /** Preset ids; replaces the list. */
999
+ egressPresets: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1000
+ /** Custom hosts (exact or `*.suffix`, port 443 unless `ports` says otherwise); replaces the list. */
1001
+ egressAllow: z.ZodOptional<z.ZodArray<z.ZodObject<{
1002
+ host: z.ZodString;
1003
+ ports: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
1004
+ }, "strip", z.ZodTypeAny, {
1005
+ host: string;
1006
+ ports?: number[] | undefined;
1007
+ }, {
1008
+ host: string;
1009
+ ports?: number[] | undefined;
1010
+ }>, "many">>;
816
1011
  name: z.ZodString;
817
1012
  gatewayEndpoint: z.ZodString;
818
1013
  gatewayPublicKey: z.ZodString;
@@ -830,21 +1025,84 @@ export declare const createVpnTunnelSchema: z.ZodObject<{
830
1025
  gatewayEndpoint: string;
831
1026
  gatewayPublicKey: string;
832
1027
  lanCidrs: string[];
1028
+ egressAllow?: {
1029
+ host: string;
1030
+ ports?: number[] | undefined;
1031
+ }[] | undefined;
1032
+ egressPresets?: string[] | undefined;
833
1033
  dnsServer?: string | null | undefined;
834
1034
  allowedDomains?: string[] | undefined;
835
1035
  tunnelCidr?: string | undefined;
836
1036
  allowPrivateEndpoint?: boolean | undefined;
1037
+ complianceMode?: "standard" | "regulated" | undefined;
1038
+ strictEgress?: boolean | undefined;
837
1039
  }, {
838
1040
  name: string;
839
1041
  gatewayEndpoint: string;
840
1042
  gatewayPublicKey: string;
841
1043
  lanCidrs: string[];
1044
+ egressAllow?: {
1045
+ host: string;
1046
+ ports?: number[] | undefined;
1047
+ }[] | undefined;
1048
+ egressPresets?: string[] | undefined;
842
1049
  dnsServer?: string | null | undefined;
843
1050
  allowedDomains?: string[] | undefined;
844
1051
  tunnelCidr?: string | undefined;
845
1052
  allowPrivateEndpoint?: boolean | undefined;
1053
+ complianceMode?: "standard" | "regulated" | undefined;
1054
+ strictEgress?: boolean | undefined;
1055
+ }>, {
1056
+ name: string;
1057
+ gatewayEndpoint: string;
1058
+ gatewayPublicKey: string;
1059
+ lanCidrs: string[];
1060
+ egressAllow?: {
1061
+ host: string;
1062
+ ports?: number[] | undefined;
1063
+ }[] | undefined;
1064
+ egressPresets?: string[] | undefined;
1065
+ dnsServer?: string | null | undefined;
1066
+ allowedDomains?: string[] | undefined;
1067
+ tunnelCidr?: string | undefined;
1068
+ allowPrivateEndpoint?: boolean | undefined;
1069
+ complianceMode?: "standard" | "regulated" | undefined;
1070
+ strictEgress?: boolean | undefined;
1071
+ }, {
1072
+ name: string;
1073
+ gatewayEndpoint: string;
1074
+ gatewayPublicKey: string;
1075
+ lanCidrs: string[];
1076
+ egressAllow?: {
1077
+ host: string;
1078
+ ports?: number[] | undefined;
1079
+ }[] | undefined;
1080
+ egressPresets?: string[] | undefined;
1081
+ dnsServer?: string | null | undefined;
1082
+ allowedDomains?: string[] | undefined;
1083
+ tunnelCidr?: string | undefined;
1084
+ allowPrivateEndpoint?: boolean | undefined;
1085
+ complianceMode?: "standard" | "regulated" | undefined;
1086
+ strictEgress?: boolean | undefined;
846
1087
  }>;
847
- export declare const updateVpnTunnelSchema: z.ZodObject<{
1088
+ /** The update body, with the egress rules enforced too. */
1089
+ export declare const updateVpnTunnelSchema: z.ZodEffects<z.ZodObject<{
1090
+ /** `regulated` needs `strictEgress`, and the operator's REGULATED_VPN_ENABLED flag. */
1091
+ complianceMode: z.ZodOptional<z.ZodEnum<["standard", "regulated"]>>;
1092
+ strictEgress: z.ZodOptional<z.ZodBoolean>;
1093
+ /** Preset ids; replaces the list. */
1094
+ egressPresets: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1095
+ /** Custom hosts (exact or `*.suffix`, port 443 unless `ports` says otherwise); replaces the list. */
1096
+ egressAllow: z.ZodOptional<z.ZodArray<z.ZodObject<{
1097
+ host: z.ZodString;
1098
+ ports: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
1099
+ }, "strip", z.ZodTypeAny, {
1100
+ host: string;
1101
+ ports?: number[] | undefined;
1102
+ }, {
1103
+ host: string;
1104
+ ports?: number[] | undefined;
1105
+ }>, "many">>;
848
1106
  name: z.ZodOptional<z.ZodString>;
849
1107
  gatewayEndpoint: z.ZodOptional<z.ZodString>;
850
1108
  gatewayPublicKey: z.ZodOptional<z.ZodString>;
@@ -861,6 +1119,11 @@ export declare const updateVpnTunnelSchema: z.ZodObject<{
861
1119
  enabled: z.ZodOptional<z.ZodBoolean>;
862
1120
  }, "strip", z.ZodTypeAny, {
863
1121
  name?: string | undefined;
1122
+ egressAllow?: {
1123
+ host: string;
1124
+ ports?: number[] | undefined;
1125
+ }[] | undefined;
1126
+ egressPresets?: string[] | undefined;
864
1127
  enabled?: boolean | undefined;
865
1128
  gatewayEndpoint?: string | undefined;
866
1129
  gatewayPublicKey?: string | undefined;
@@ -869,8 +1132,15 @@ export declare const updateVpnTunnelSchema: z.ZodObject<{
869
1132
  allowedDomains?: string[] | undefined;
870
1133
  tunnelCidr?: string | undefined;
871
1134
  allowPrivateEndpoint?: boolean | undefined;
1135
+ complianceMode?: "standard" | "regulated" | undefined;
1136
+ strictEgress?: boolean | undefined;
872
1137
  }, {
873
1138
  name?: string | undefined;
1139
+ egressAllow?: {
1140
+ host: string;
1141
+ ports?: number[] | undefined;
1142
+ }[] | undefined;
1143
+ egressPresets?: string[] | undefined;
874
1144
  enabled?: boolean | undefined;
875
1145
  gatewayEndpoint?: string | undefined;
876
1146
  gatewayPublicKey?: string | undefined;
@@ -879,6 +1149,71 @@ export declare const updateVpnTunnelSchema: z.ZodObject<{
879
1149
  allowedDomains?: string[] | undefined;
880
1150
  tunnelCidr?: string | undefined;
881
1151
  allowPrivateEndpoint?: boolean | undefined;
1152
+ complianceMode?: "standard" | "regulated" | undefined;
1153
+ strictEgress?: boolean | undefined;
1154
+ }>, {
1155
+ name?: string | undefined;
1156
+ egressAllow?: {
1157
+ host: string;
1158
+ ports?: number[] | undefined;
1159
+ }[] | undefined;
1160
+ egressPresets?: string[] | undefined;
1161
+ enabled?: boolean | undefined;
1162
+ gatewayEndpoint?: string | undefined;
1163
+ gatewayPublicKey?: string | undefined;
1164
+ lanCidrs?: string[] | undefined;
1165
+ dnsServer?: string | null | undefined;
1166
+ allowedDomains?: string[] | undefined;
1167
+ tunnelCidr?: string | undefined;
1168
+ allowPrivateEndpoint?: boolean | undefined;
1169
+ complianceMode?: "standard" | "regulated" | undefined;
1170
+ strictEgress?: boolean | undefined;
1171
+ }, {
1172
+ name?: string | undefined;
1173
+ egressAllow?: {
1174
+ host: string;
1175
+ ports?: number[] | undefined;
1176
+ }[] | undefined;
1177
+ egressPresets?: string[] | undefined;
1178
+ enabled?: boolean | undefined;
1179
+ gatewayEndpoint?: string | undefined;
1180
+ gatewayPublicKey?: string | undefined;
1181
+ lanCidrs?: string[] | undefined;
1182
+ dnsServer?: string | null | undefined;
1183
+ allowedDomains?: string[] | undefined;
1184
+ tunnelCidr?: string | undefined;
1185
+ allowPrivateEndpoint?: boolean | undefined;
1186
+ complianceMode?: "standard" | "regulated" | undefined;
1187
+ strictEgress?: boolean | undefined;
1188
+ }>;
1189
+ /**
1190
+ * The strict-egress allow-list (`VpnJobConfig.egress`; docs in vpn-egress.ts).
1191
+ * Field-level shape and size bounds only; the host rules (no IP literals, no
1192
+ * bare `*`, no trailing dot ...) live in `validateVpnEgressPolicy` so the
1193
+ * server, the CLI and the agent apply the identical checks and report every
1194
+ * problem at once. Tunnel settings (PR-D) will embed this.
1195
+ */
1196
+ export declare const vpnEgressPolicySchema: z.ZodObject<{
1197
+ allow: z.ZodArray<z.ZodObject<{
1198
+ host: z.ZodString;
1199
+ ports: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
1200
+ }, "strip", z.ZodTypeAny, {
1201
+ host: string;
1202
+ ports?: number[] | undefined;
1203
+ }, {
1204
+ host: string;
1205
+ ports?: number[] | undefined;
1206
+ }>, "many">;
1207
+ }, "strip", z.ZodTypeAny, {
1208
+ allow: {
1209
+ host: string;
1210
+ ports?: number[] | undefined;
1211
+ }[];
1212
+ }, {
1213
+ allow: {
1214
+ host: string;
1215
+ ports?: number[] | undefined;
1216
+ }[];
882
1217
  }>;
883
1218
  /** `POST /agents/vpn/report` (apps/admin-api) — what the agent that holds
884
1219
  * an org's tunnel for a build tells the control plane about it. The
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"}
package/dist/schemas.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { z } from "zod";
2
+ import { ENFORCEMENT_LEVELS, MAX_VPN_FEATURES, VPN_FEATURE_ID_PATTERN, VPN_HOST_POLICIES } from "./agent-readiness.js";
3
+ import { VPN_COMPLIANCE_MODES, VPN_EGRESS_MAX_CUSTOM_RULES, VPN_EGRESS_MAX_PRESETS, validateEgressSettings } from "./vpn-egress-presets.js";
2
4
  export const platformSchema = z.enum(["ios", "android", "flutter", "react-native"]);
3
5
  export const appforgeWorkflowSchema = z.object({
4
6
  name: z.string().optional(),
@@ -456,6 +458,20 @@ export const agentHeartbeatSchema = z.object({
456
458
  * predates the VPN feature omits it and stays `vpnCapable = 0`, so it
457
459
  * is never offered a VPN build. */
458
460
  vpnCapable: z.boolean().optional(),
461
+ /** VPN features this agent can enforce (migrations/0043) — ids matching
462
+ * `VPN_FEATURE_ID_PATTERN`, at most `MAX_VPN_FEATURES`. Like `vpnCapable`
463
+ * the stored value is derived from THIS heartbeat alone: omitting it (an
464
+ * older agent) means "no features", never "keep the last value". A
465
+ * malformed list is rejected (400) rather than half-trusted. */
466
+ vpnFeatures: z.array(z.string().regex(VPN_FEATURE_ID_PATTERN)).max(MAX_VPN_FEATURES).optional(),
467
+ /** Build-isolation level this agent enforces (migrations/0043). Same
468
+ * omit-means-default (`none`) rule as `vpnFeatures`. */
469
+ enforcementLevel: z.enum(ENFORCEMENT_LEVELS).optional(),
470
+ /** Whether a GitHub Actions self-hosted runner shares this machine
471
+ * (migrations/0047). Like `vpnFeatures`, stored from THIS heartbeat alone:
472
+ * omitted (an older agent, or a check that failed) means "unknown", never
473
+ * "keep the last value", and unknown refuses a dedicated host. */
474
+ coHostedRunner: z.boolean().optional(),
459
475
  });
460
476
  // `POST /builds/:buildId/logs` (apps/admin-api/src/routes/agents.ts) —
461
477
  // one call per flushed line/burst from `packages/agent/src/lib/
@@ -543,6 +559,21 @@ export const setAgentRemoteControlSchema = z.object({
543
559
  export const setAgentVpnApprovedSchema = z.object({
544
560
  approved: z.boolean(),
545
561
  });
562
+ /** `PATCH /orgs/:orgId/agents/:agentId/dedicated` (apps/admin-api) — the
563
+ * only way `Agent.dedicatedOrgId` ever changes (migrations/0043). An
564
+ * operator taint, deliberately not in agentHeartbeatSchema/agentEnrollSchema:
565
+ * an agent must never be able to widen or lift its own restriction. `null`
566
+ * clears it (the Mac goes back to being shared). */
567
+ export const setAgentDedicatedOrgSchema = z.object({
568
+ dedicatedOrgId: z.string().min(1).max(128).nullable(),
569
+ });
570
+ /** `PATCH /orgs/:orgId/vpn-hosts/:tunnelOrgId/policy` (apps/admin-api, platform
571
+ * admins only) — the only way `VpnTunnel.hostPolicy` ever changes
572
+ * (migrations/0047). Deliberately not in `updateVpnTunnelSchema`: an org
573
+ * admin must not be able to loosen the placement the operator set. */
574
+ export const setVpnHostPolicySchema = z.object({
575
+ hostPolicy: z.enum(VPN_HOST_POLICIES),
576
+ });
546
577
  /** `PATCH /orgs/:orgId/agents/:agentId/device-stream-hostname` (apps/
547
578
  * admin-api)'s request body — `hostname: null` clears it, going back
548
579
  * to the global DEVICE_STREAM_WS_URL fallback (see migrations/0034's
@@ -569,7 +600,45 @@ export const startVncSessionSchema = z.object({
569
600
  // and the route all apply the identical checks, with every problem
570
601
  // reported at once.
571
602
  const vpnNameSchema = z.string().min(1).max(64, "must be at most 64 characters");
572
- export const createVpnTunnelSchema = z.object({
603
+ /**
604
+ * The egress-policy fields of a tunnel (migrations/0045), shared by create and
605
+ * update. The field shapes here are shape and size bounds only; which preset ids
606
+ * exist and which hosts are acceptable (no bare `*`, no IP literals ...) is
607
+ * `validateEgressSettings` (vpn-egress-presets.ts), which `refineVpnEgressSettings`
608
+ * applies on top so a caller that relies on these schemas alone cannot accept
609
+ * what the route would refuse. Whether `regulated` may be set at all is the
610
+ * route's call (an operator flag), not the schema's.
611
+ */
612
+ const vpnEgressSettingsFields = {
613
+ /** `regulated` needs `strictEgress`, and the operator's REGULATED_VPN_ENABLED flag. */
614
+ complianceMode: z.enum(VPN_COMPLIANCE_MODES).optional(),
615
+ strictEgress: z.boolean().optional(),
616
+ /** Preset ids; replaces the list. */
617
+ egressPresets: z.array(z.string().min(1).max(64)).max(VPN_EGRESS_MAX_PRESETS).optional(),
618
+ /** Custom hosts (exact or `*.suffix`, port 443 unless `ports` says otherwise); replaces the list. */
619
+ egressAllow: z
620
+ .array(z.object({
621
+ host: z.string().min(1).max(253),
622
+ ports: z.array(z.number().int().min(1).max(65535)).max(16).optional(),
623
+ }))
624
+ .max(VPN_EGRESS_MAX_CUSTOM_RULES)
625
+ .optional(),
626
+ };
627
+ /** Adds `validateEgressSettings`' host/preset/merge rules to a tunnel schema. Problem strings never echo a rejected value. */
628
+ function refineVpnEgressSettings(input, ctx) {
629
+ for (const message of validateEgressSettings({ egressPresets: input.egressPresets, egressAllow: input.egressAllow })) {
630
+ const field = message.startsWith("egressPresets") ? "egressPresets" : message.startsWith("egressAllow") ? "egressAllow" : null;
631
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message, path: field ? [field] : [] });
632
+ }
633
+ }
634
+ /**
635
+ * The shape-only form of the create/update bodies, WITHOUT the egress rules. The
636
+ * API route parses with these and then runs `resolveEgressChange`, which applies
637
+ * the same `validateEgressSettings` but answers with a structured 400 listing
638
+ * every problem (`errors`) rather than a generic schema error. Anything else
639
+ * should use `createVpnTunnelSchema` / `updateVpnTunnelSchema` below.
640
+ */
641
+ export const createVpnTunnelBodySchema = z.object({
573
642
  name: vpnNameSchema,
574
643
  gatewayEndpoint: z.string().min(1).max(300),
575
644
  gatewayPublicKey: z.string().min(1).max(64),
@@ -582,8 +651,9 @@ export const createVpnTunnelSchema = z.object({
582
651
  /** Lab mode — allow a private (RFC1918/CGNAT) gateway endpoint. Setting it
583
652
  * `true` is platform-admin only (enforced by the route, not here). */
584
653
  allowPrivateEndpoint: z.boolean().optional(),
654
+ ...vpnEgressSettingsFields,
585
655
  });
586
- export const updateVpnTunnelSchema = z.object({
656
+ export const updateVpnTunnelBodySchema = z.object({
587
657
  name: vpnNameSchema.optional(),
588
658
  gatewayEndpoint: z.string().min(1).max(300).optional(),
589
659
  gatewayPublicKey: z.string().min(1).max(64).optional(),
@@ -598,6 +668,26 @@ export const updateVpnTunnelSchema = z.object({
598
668
  * instead of queueing); `true` re-enables it, back to `pending` until
599
669
  * the next handshake proves it works. */
600
670
  enabled: z.boolean().optional(),
671
+ ...vpnEgressSettingsFields,
672
+ });
673
+ /** The create body, with the egress rules (bare `*`, IP literals, unknown presets, merged port limit) enforced too. */
674
+ export const createVpnTunnelSchema = createVpnTunnelBodySchema.superRefine(refineVpnEgressSettings);
675
+ /** The update body, with the egress rules enforced too. */
676
+ export const updateVpnTunnelSchema = updateVpnTunnelBodySchema.superRefine(refineVpnEgressSettings);
677
+ /**
678
+ * The strict-egress allow-list (`VpnJobConfig.egress`; docs in vpn-egress.ts).
679
+ * Field-level shape and size bounds only; the host rules (no IP literals, no
680
+ * bare `*`, no trailing dot ...) live in `validateVpnEgressPolicy` so the
681
+ * server, the CLI and the agent apply the identical checks and report every
682
+ * problem at once. Tunnel settings (PR-D) will embed this.
683
+ */
684
+ export const vpnEgressPolicySchema = z.object({
685
+ allow: z
686
+ .array(z.object({
687
+ host: z.string().min(1).max(253),
688
+ ports: z.array(z.number().int().min(1).max(65535)).max(16).optional(),
689
+ }))
690
+ .max(512),
601
691
  });
602
692
  /** `POST /agents/vpn/report` (apps/admin-api) — what the agent that holds
603
693
  * an org's tunnel for a build tells the control plane about it. The